Skip to content

Commit 2dd6a13

Browse files
Add DFS with parent-completion constraint for DAG traversal (#6467)
* Add DFS with parent-completion constraint for DAG traversal * warning in PartitionProblem.java affecting tests * added clang-format and updated javadoc * optimized imports and rechecked camelCase format in tests * removed .* import and made small visual change * replaced a inline return with correct {} block * Removed changed in PartitionProblem.java, Renamed class name to be straightforward about the implementation.Added full names instead of shortcuts, and included record. * updated for clang format --------- Co-authored-by: Deniz Altunkapan <[email protected]>
1 parent 16345cb commit 2dd6a13

File tree

2 files changed

+253
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)