Skip to content

Commit bdd85a8

Browse files
Add DFS with parent-completion constraint for DAG traversal
1 parent 57c6b03 commit bdd85a8

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.*;
4+
5+
/**
6+
* Graph traversals and utilities.
7+
*
8+
* <p>This class includes a DFS variant that visits a successor only when
9+
* <b>all</b> of its predecessors have already been visited. For each step,
10+
* it emits an event either for a visit (with an increasing order) or for a skip
11+
* explaining that not all parents were visited yet.</p>
12+
*
13+
* <p>Input is an adjacency list of successors. If a predecessor map is not
14+
* provided, it is derived once from the successors map.</p>
15+
*/
16+
public final class GraphTraversal {
17+
18+
private GraphTraversal() {
19+
// utility class
20+
}
21+
22+
/** An event emitted by the traversal: either a VISIT with an order, or a SKIP with a note. */
23+
public static final class TraversalEvent<T> {
24+
private final T node;
25+
private final Integer order; // non-null for visit, null for skip
26+
private final String note; // non-null for skip, null for visit
27+
28+
private TraversalEvent(T node, Integer order, String note) {
29+
this.node = node;
30+
this.order = order;
31+
this.note = note;
32+
}
33+
34+
/** A visit event with an increasing order (0,1,2,...) */
35+
public static <T> TraversalEvent<T> visit(T node, int order) {
36+
return new TraversalEvent<>(Objects.requireNonNull(node), order, null);
37+
}
38+
39+
/** A skip event with an explanatory note (e.g., not all parents visited yet). */
40+
public static <T> TraversalEvent<T> skip(T node, String note) {
41+
return new TraversalEvent<>(Objects.requireNonNull(node), null, Objects.requireNonNull(note));
42+
}
43+
44+
public boolean isVisit() { return order != null; }
45+
public boolean isSkip() { return order == null; }
46+
public T node() { return node; }
47+
public Integer order() { return order; }
48+
public String note() { return note; }
49+
50+
@Override public String toString() {
51+
return isVisit() ? "VISIT(" + node + ", order=" + order + ")" : "SKIP(" + node + ", " + note + ")";
52+
}
53+
}
54+
55+
/**
56+
* DFS (recursive) that records the order of first visit starting at {@code start},
57+
* but only recurses to a child when <b>all</b> its predecessors have been visited.
58+
* If a child is encountered early (some parent unvisited), a SKIP event is recorded.
59+
*
60+
* <p>Equivalent idea to the Python pseudo in the user's description (with successors and predecessors),
61+
* but implemented in Java and returning a sequence of {@link TraversalEvent}s.</p>
62+
*
63+
* @param successors adjacency list: for each node, its outgoing neighbors
64+
* @param start start node
65+
* @return immutable list of traversal events (VISITs with monotonically increasing order and SKIPs with messages)
66+
* @throws IllegalArgumentException if {@code successors} is null
67+
*/
68+
public static <T> List<TraversalEvent<T>> dfsRecursiveOrder(Map<T, List<T>> successors, T start) {
69+
if (successors == null) {
70+
throw new IllegalArgumentException("successors must not be null");
71+
}
72+
// derive predecessors once
73+
Map<T, List<T>> predecessors = derivePredecessors(successors);
74+
return dfsRecursiveOrder(successors, predecessors, start);
75+
}
76+
77+
/**
78+
* Same as {@link #dfsRecursiveOrder(Map, Object)} but with an explicit predecessors map.
79+
*/
80+
public static <T> List<TraversalEvent<T>> dfsRecursiveOrder(
81+
Map<T, List<T>> successors, Map<T, List<T>> predecessors, T start) {
82+
83+
if (successors == null || predecessors == null) {
84+
throw new IllegalArgumentException("successors and predecessors must not be null");
85+
}
86+
if (start == null) {
87+
return List.of();
88+
}
89+
if (!successors.containsKey(start) && !appearsAnywhere(successors, start)) {
90+
return List.of(); // start not present in graph
91+
}
92+
93+
List<TraversalEvent<T>> events = new ArrayList<>();
94+
Set<T> visited = new HashSet<>();
95+
int[] order = {0};
96+
dfs(start, successors, predecessors, visited, order, events);
97+
return Collections.unmodifiableList(events);
98+
}
99+
100+
private static <T> void dfs(
101+
T u,
102+
Map<T, List<T>> succ,
103+
Map<T, List<T>> pred,
104+
Set<T> visited,
105+
int[] order,
106+
List<TraversalEvent<T>> out) {
107+
108+
if (!visited.add(u)) {
109+
return; // already visited
110+
}
111+
out.add(TraversalEvent.visit(u, order[0]++)); // record visit and increment
112+
113+
for (T v : succ.getOrDefault(u, List.of())) {
114+
if (visited.contains(v)) {
115+
continue;
116+
}
117+
if (allParentsVisited(v, visited, pred)) {
118+
dfs(v, succ, pred, visited, order, out);
119+
} else {
120+
out.add(TraversalEvent.skip(v, "⛔ Skipping " + v + ": not all parents are visited yet."));
121+
// do not mark visited; it may be visited later from another parent
122+
}
123+
}
124+
}
125+
126+
private static <T> boolean allParentsVisited(T node, Set<T> visited, Map<T, List<T>> pred) {
127+
for (T p : pred.getOrDefault(node, List.of())) {
128+
if (!visited.contains(p)) {
129+
return false;
130+
}
131+
}
132+
return true;
133+
}
134+
135+
private static <T> boolean appearsAnywhere(Map<T, List<T>> succ, T node) {
136+
if (succ.containsKey(node)) return true;
137+
for (List<T> nbrs : succ.values()) {
138+
if (nbrs != null && nbrs.contains(node)) return true;
139+
}
140+
return false;
141+
}
142+
143+
private static <T> Map<T, List<T>> derivePredecessors(Map<T, List<T>> succ) {
144+
Map<T, List<T>> pred = new HashMap<>();
145+
// ensure keys exist for all nodes appearing anywhere
146+
for (Map.Entry<T, List<T>> e : succ.entrySet()) {
147+
pred.computeIfAbsent(e.getKey(), k -> new ArrayList<>());
148+
for (T v : e.getValue()) {
149+
pred.computeIfAbsent(v, k -> new ArrayList<>()).add(e.getKey());
150+
}
151+
}
152+
return pred;
153+
}
154+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import com.thealgorithms.graph.GraphTraversal.TraversalEvent;
7+
import java.util.*;
8+
import org.junit.jupiter.api.Test;
9+
10+
class GraphTraversalTest {
11+
12+
// A -> B, A -> C, B -> D, C -> D (classic diamond)
13+
private static Map<String, List<String>> diamond() {
14+
Map<String, List<String>> g = new LinkedHashMap<>();
15+
g.put("A", List.of("B", "C"));
16+
g.put("B", List.of("D"));
17+
g.put("C", List.of("D"));
18+
g.put("D", List.of());
19+
return g;
20+
}
21+
22+
@Test
23+
void dfsRecursiveOrder_emitsSkipUntilAllParentsVisited() {
24+
List<TraversalEvent<String>> events = GraphTraversal.dfsRecursiveOrder(diamond(), "A");
25+
26+
// Expect visits in order and a skip for first time we meet D (via B) before C is visited.
27+
var visits = events.stream().filter(TraversalEvent::isVisit).toList();
28+
var skips = events.stream().filter(TraversalEvent::isSkip).toList();
29+
30+
// Visits should be A(0), B(1), C(2), D(3) in some deterministic order given adjacency
31+
assertThat(visits).hasSize(4);
32+
assertThat(visits.get(0).node()).isEqualTo("A");
33+
assertThat(visits.get(0).order()).isEqualTo(0);
34+
assertThat(visits.get(1).node()).isEqualTo("B");
35+
assertThat(visits.get(1).order()).isEqualTo(1);
36+
assertThat(visits.get(2).node()).isEqualTo("C");
37+
assertThat(visits.get(2).order()).isEqualTo(2);
38+
assertThat(visits.get(3).node()).isEqualTo("D");
39+
assertThat(visits.get(3).order()).isEqualTo(3);
40+
41+
// One skip when we first encountered D from B (before C was visited)
42+
assertThat(skips).hasSize(1);
43+
assertThat(skips.get(0).node()).isEqualTo("D");
44+
assertThat(skips.get(0).note()).contains("not all parents");
45+
}
46+
47+
@Test
48+
void returnsEmptyWhenStartNotInGraph() {
49+
Map<Integer, List<Integer>> g = Map.of(1, List.of(2), 2, List.of(1));
50+
assertThat(GraphTraversal.dfsRecursiveOrder(g, 99)).isEmpty();
51+
}
52+
53+
@Test
54+
void nullSuccessorsThrows() {
55+
assertThrows(IllegalArgumentException.class, () -> GraphTraversal.dfsRecursiveOrder(null, "A"));
56+
}
57+
58+
@Test
59+
void worksWithExplicitPredecessors() {
60+
Map<Integer, List<Integer>> succ = new HashMap<>();
61+
succ.put(10, List.of(20));
62+
succ.put(20, List.of(30));
63+
succ.put(30, List.of());
64+
65+
Map<Integer, List<Integer>> pred = new HashMap<>();
66+
pred.put(10, List.of());
67+
pred.put(20, List.of(10));
68+
pred.put(30, List.of(20));
69+
70+
var events = GraphTraversal.dfsRecursiveOrder(succ, pred, 10);
71+
var visitNodes = events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node).toList();
72+
assertThat(visitNodes).containsExactly(10, 20, 30);
73+
}
74+
75+
@Test
76+
void cycleProducesSkipsButNoInfiniteRecursion() {
77+
Map<String, List<String>> succ = new LinkedHashMap<>();
78+
succ.put("X", List.of("Y"));
79+
succ.put("Y", List.of("X")); // 2-cycle
80+
81+
var events = GraphTraversal.dfsRecursiveOrder(succ, "X");
82+
// Only X is visited; encountering Y from X causes skip because Y's parent X is visited,
83+
// but when recursing to Y we'd hit back to X (already visited) and stop; no infinite loop.
84+
assertThat(events.stream().anyMatch(TraversalEvent::isVisit)).isTrue();
85+
assertThat(events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node)).contains("X");
86+
}
87+
}

0 commit comments

Comments
 (0)