Skip to content

Commit c88369c

Browse files
committed
Implemented Tarjan's algorithm for removing dead-end nodes.
- Previous two pass algorithm didn't work in all cases. - Tarjan's is a single pass, and ~2x the speed.
1 parent d3af572 commit c88369c

File tree

3 files changed

+221
-84
lines changed

3 files changed

+221
-84
lines changed

core/src/main/java/com/graphhopper/routing/util/PrepareRoutingSubnetworks.java

Lines changed: 19 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
import java.util.*;
2828
import java.util.Map.Entry;
2929
import java.util.concurrent.atomic.AtomicInteger;
30+
3031
import org.slf4j.Logger;
3132
import org.slf4j.LoggerFactory;
3233

34+
import gnu.trove.list.array.TIntArrayList;
35+
3336
/**
3437
* Removes nodes which are not part of the largest network. Ie. mostly nodes with no edges at all
3538
* but also small subnetworks which are nearly always bugs in OSM data or indicate otherwise
@@ -146,7 +149,7 @@ protected final boolean checkAdjacent( EdgeIteratorState iter )
146149
}
147150

148151
/**
149-
* Deletes all but the larges subnetworks.
152+
* Deletes all but the largest subnetworks.
150153
*/
151154
void keepLargeNetworks( Map<Integer, Integer> map )
152155
{
@@ -231,86 +234,29 @@ int removeZeroDegreeNodes()
231234

232235
/**
233236
* Clean small networks that will be never be visited by this explorer See #86 For example,
234-
* small areas like parkings are sometimes connected to the whole network through one-way road
237+
* small areas like parking lots are sometimes connected to the whole network through a one-way road.
235238
* This is clearly an error - but is causes the routing to fail when point get connected to this
236-
* small area This routines removed all these points from the graph The algorithm is to through
237-
* the graph, build the network map and for each small map remove the network
239+
* small area. This routines removed all these points from the graph.
238240
* <p/>
239-
* @return removed nodes;
241+
* @return number of removed nodes;
240242
*/
241243
public int removeDeadEndUnvisitedNetworks( final FlagEncoder encoder )
242244
{
243-
int removed = 0;
244-
removed += removeDeadEndUnvisitedNetworks(g.createEdgeExplorer(new DefaultEdgeFilter(encoder, true, false)));
245-
removed += removeDeadEndUnvisitedNetworks(g.createEdgeExplorer(new DefaultEdgeFilter(encoder, false, true)));
246-
return removed;
247-
}
248-
249-
private static <K, V extends Comparable<V>> Map<K, V> sortByValues( final Map<K, V> map )
250-
{
251-
Comparator<K> valueComparator = new Comparator<K>()
252-
{
253-
@Override
254-
public int compare( K k1, K k2 )
255-
{
256-
int compare = map.get(k2).compareTo(map.get(k1));
257-
if (compare == 0)
258-
return 1;
259-
else
260-
return compare;
261-
}
262-
};
263-
Map<K, V> sortedByValues = new TreeMap<K, V>(valueComparator);
264-
sortedByValues.putAll(map);
265-
return sortedByValues;
266-
}
267-
268-
public int removeDeadEndUnvisitedNetworks( final EdgeExplorer explorer )
269-
{
270-
final AtomicInteger removed = new AtomicInteger(0);
271-
272-
// Find subnetworks according to this explorer
273-
// Sort the map by largest networks first
274-
Map<Integer, Integer> map = sortByValues(findSubnetworks(explorer));
275-
if (map.size() < 2)
276-
return 0;
277-
278-
// big networks will populate bs first so these nodes won't be deleted
279-
final GHBitSetImpl bs = new GHBitSetImpl(g.getNodes());
280-
boolean first = true;
281-
for (Entry<Integer, Integer> e : map.entrySet())
282-
{
283-
int mapStart = e.getKey();
284-
int subnetSize = e.getValue();
285-
final boolean removeNetwork = !first && (subnetSize < minOnewayNetworkSize);
286-
if (first)
287-
first = false;
288-
289-
if (removeNetwork)
290-
logger.debug("Removing dead-end network: " + subnetSize + " nodes starting from nodeid=" + mapStart);
245+
// Partition g into strongly connected components using Tarjan's Algorithm.
246+
final EdgeFilter filter = new DefaultEdgeFilter(encoder, false, true);
247+
List<TIntArrayList> components = new TarjansStronglyConnectedComponentsAlgorithm(g, filter).findComponents();
291248

292-
new XFirstSearch()
293-
{
294-
@Override
295-
protected GHBitSet createBitSet()
296-
{
297-
return bs;
298-
}
249+
// remove components less than minimum size
250+
int removed = 0;
251+
for (TIntArrayList component : components) {
299252

300-
@Override
301-
protected final boolean goFurther( int nodeId )
302-
{
303-
if (removeNetwork)
304-
{
305-
// This remaining node is member of a small disconnected network
306-
g.markNodeRemoved(nodeId);
307-
removed.incrementAndGet();
308-
}
309-
return super.goFurther(nodeId);
253+
if (component.size() < minOnewayNetworkSize) {
254+
for (int i = 0; i < component.size(); i++) {
255+
g.markNodeRemoved(component.get(i));
256+
removed ++;
310257
}
311-
}.start(explorer, mapStart, false);
258+
}
312259
}
313-
314-
return removed.get();
260+
return removed;
315261
}
316262
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.graphhopper.routing.util;
2+
3+
import com.graphhopper.coll.GHBitSetImpl;
4+
import com.graphhopper.storage.GraphStorage;
5+
import com.graphhopper.util.EdgeIterator;
6+
import gnu.trove.list.array.TIntArrayList;
7+
import gnu.trove.stack.array.TIntArrayStack;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Stack;
12+
13+
/**
14+
* Implementation of Tarjan's algorithm using an explicit stack.
15+
* (The traditional recursive approach runs into stack overflow pretty quickly.)
16+
*
17+
* Used for finding strongly strongly connected components to detect dead-ends.
18+
*
19+
* http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
20+
*/
21+
public class TarjansStronglyConnectedComponentsAlgorithm {
22+
23+
private final GraphStorage g;
24+
private final TIntArrayStack nodeStack;
25+
private final GHBitSetImpl onStack;
26+
private final int[] nodeIndex;
27+
private final int[] nodeLowLink;
28+
private final ArrayList<TIntArrayList> components = new ArrayList<TIntArrayList>();
29+
30+
private int index = 1;
31+
private final EdgeFilter edgeFilter;
32+
33+
public TarjansStronglyConnectedComponentsAlgorithm(final GraphStorage g, final EdgeFilter edgeFilter) {
34+
this.g = g;
35+
this.nodeStack = new TIntArrayStack();
36+
this.onStack = new GHBitSetImpl(g.getNodes());
37+
this.nodeIndex = new int[g.getNodes()];
38+
this.nodeLowLink = new int[g.getNodes()];
39+
this.edgeFilter = edgeFilter;
40+
}
41+
42+
/**
43+
* Find and return list of all strongly connected components in g.
44+
*/
45+
public List<TIntArrayList> findComponents() {
46+
47+
int nodes = g.getNodes();
48+
for (int start = 0; start < nodes; start++) {
49+
if (nodeIndex[start] == 0 && !g.isNodeRemoved(start)) {
50+
strongConnect(start);
51+
}
52+
}
53+
54+
return components;
55+
}
56+
57+
// Find all components reachable from firstNode, add them to 'components'
58+
private void strongConnect(int firstNode) {
59+
final Stack<TarjanState> stateStack = new Stack<TarjanState>();
60+
stateStack.push(TarjanState.startState(firstNode));
61+
62+
// nextState label is equivalent to the function entry point in the recursive Tarjan's algorithm.
63+
nextState:
64+
65+
while (!stateStack.empty()) {
66+
TarjanState state = stateStack.pop();
67+
final int start = state.start;
68+
final EdgeIterator iter;
69+
70+
if (state.isStart()) {
71+
// We're traversing a new node 'start'. Set the depth index for this node to the smallest unused index.
72+
nodeIndex[start] = index;
73+
nodeLowLink[start] = index;
74+
index ++;
75+
nodeStack.push(start);
76+
onStack.set(start);
77+
78+
iter = g.createEdgeExplorer(edgeFilter).setBaseNode(start);
79+
80+
} else { // if (state.isResume()) {
81+
82+
// We're resuming iteration over the next child of 'start', set lowLink as appropriate.
83+
iter = state.iter;
84+
85+
int prevConnectedId = iter.getAdjNode();
86+
nodeLowLink[start] = Math.min(nodeLowLink[start], nodeLowLink[prevConnectedId]);
87+
}
88+
89+
// Each element (excluding the first) in the current component should be able to find
90+
// a successor with a lower nodeLowLink.
91+
while (iter.next())
92+
{
93+
int connectedId = iter.getAdjNode();
94+
if (nodeIndex[connectedId] == 0) {
95+
// Push resume and start states onto state stack to continue our DFS through the graph after the jump.
96+
// Ideally we'd just call strongConnectIterative(connectedId);
97+
stateStack.push(TarjanState.resumeState(start, iter));
98+
stateStack.push(TarjanState.startState(connectedId));
99+
continue nextState;
100+
} else if (onStack.contains(connectedId)) {
101+
nodeLowLink[start] = Math.min(nodeLowLink[start], nodeIndex[connectedId]);
102+
}
103+
}
104+
105+
// If nodeLowLink == nodeIndex, then we are the first element in a component.
106+
// Add all nodes higher up on nodeStack to this component.
107+
if (nodeIndex[start] == nodeLowLink[start]) {
108+
TIntArrayList component = new TIntArrayList();
109+
int node;
110+
while ((node = nodeStack.pop()) != start) {
111+
component.add(node);
112+
onStack.clear(node);
113+
}
114+
component.add(start);
115+
onStack.clear(start);
116+
117+
components.add(component);
118+
}
119+
}
120+
}
121+
122+
// Internal stack state of algorithm, used to avoid recursive function calls and hitting stack overflow exceptions.
123+
// State is either 'start' for new nodes or 'resume' for partially traversed nodes.
124+
private static class TarjanState {
125+
final int start;
126+
final EdgeIterator iter;
127+
128+
// Iterator only present in 'resume' state.
129+
boolean isStart() { return iter == null; }
130+
131+
private TarjanState(final int start, final EdgeIterator iter) {
132+
this.start = start;
133+
this.iter = iter;
134+
}
135+
136+
public static TarjanState startState(int start) {
137+
return new TarjanState(start, null);
138+
}
139+
140+
public static TarjanState resumeState(int start, EdgeIterator iter) {
141+
return new TarjanState(start, iter);
142+
}
143+
}
144+
}

core/src/test/java/com/graphhopper/routing/util/PrepareRoutingSubnetworksTest.java

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import com.graphhopper.storage.GraphStorage;
2222
import com.graphhopper.util.EdgeExplorer;
2323
import com.graphhopper.util.GHUtility;
24+
25+
import gnu.trove.list.array.TIntArrayList;
2426
import java.util.Arrays;
27+
import java.util.List;
2528
import java.util.Map;
2629
import org.junit.*;
2730
import static org.junit.Assert.*;
28-
2931
/**
3032
*
3133
* @author Peter Karich
@@ -162,21 +164,31 @@ GraphStorage createDeadEndUnvisitedNetworkGraph( EncodingManager em )
162164
g.edge(7, 8, 1, false);
163165
g.edge(8, 9, 1, true);
164166
g.edge(9, 10, 1, true);
167+
165168
return g;
166169
}
167170

168-
@Test
169-
public void testRemoveDeadEndUnvisitedNetworksOneWay()
171+
GraphStorage createTarjanTestGraph()
170172
{
171-
GraphStorage g = createDeadEndUnvisitedNetworkGraph(em);
172-
PrepareRoutingSubnetworks instance = new PrepareRoutingSubnetworks(g, em).setMinOnewayNetworkSize(3);
173-
FlagEncoder encoder = em.getSingle();
174-
EdgeExplorer explorer = g.createEdgeExplorer(new DefaultEdgeFilter(encoder, false, true));
175-
int removed = instance.removeDeadEndUnvisitedNetworks(explorer);
176-
assertEquals(2, removed);
173+
GraphStorage g = createGraph(em);
177174

178-
g.optimize();
179-
assertEquals(9, g.getNodes());
175+
g.edge(1, 2, 1, false);
176+
g.edge(2, 3, 1, false);
177+
g.edge(3, 1, 1, false);
178+
179+
g.edge(4, 2, 1, false);
180+
g.edge(4, 3, 1, false);
181+
g.edge(4, 5, 1, true);
182+
g.edge(5, 6, 1, false);
183+
184+
g.edge(6, 3, 1, false);
185+
g.edge(6, 7, 1, true);
186+
187+
g.edge(8, 5, 1, false);
188+
g.edge(8, 7, 1, false);
189+
g.edge(8, 8, 1, false);
190+
191+
return g;
180192
}
181193

182194
@Test
@@ -193,4 +205,39 @@ public void testRemoveDeadEndUnvisitedNetworks()
193205
g.optimize();
194206
assertEquals(8, g.getNodes());
195207
}
208+
209+
@Test
210+
public void testTarjan()
211+
{
212+
GraphStorage g = createSubnetworkTestGraph();
213+
214+
// Requires a single vehicle type, otherwise we throw.
215+
final FlagEncoder flagEncoder = em.getSingle();
216+
final EdgeFilter filter = new DefaultEdgeFilter(flagEncoder, false, true);
217+
218+
TarjansStronglyConnectedComponentsAlgorithm tarjan = new TarjansStronglyConnectedComponentsAlgorithm(g, filter);
219+
220+
List<TIntArrayList> components = tarjan.findComponents();
221+
222+
assertEquals(4, components.size());
223+
assertEquals(new TIntArrayList(new int[]{ 13, 5, 3, 7, 0 }), components.get(0));
224+
assertEquals(new TIntArrayList(new int[]{ 2, 4, 12, 11, 8, 1 }), components.get(1));
225+
assertEquals(new TIntArrayList(new int[] {10, 14, 6}), components.get(2));
226+
assertEquals(new TIntArrayList(new int[] {9}), components.get(3));
227+
}
228+
229+
// Previous two-pass implementation failed on 1 -> 2 -> 0
230+
@Test
231+
public void testNodeOrderingRegression() {
232+
// 1 -> 2 -> 0
233+
GraphStorage g = createGraph(em);
234+
g.edge(1, 2, 1, false);
235+
g.edge(2, 0, 1, false);
236+
237+
PrepareRoutingSubnetworks instance = new PrepareRoutingSubnetworks(g, em).setMinOnewayNetworkSize(2);
238+
int removed = instance.removeDeadEndUnvisitedNetworks(em.getSingle());
239+
240+
assertEquals(3, removed);
241+
}
242+
196243
}

0 commit comments

Comments
 (0)