Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2dd6a13

Browse files
StathisVeinoglouDenizAltunkapan
andauthored
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 <93663085+DenizAltunkapan@users.noreply.github.com>
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)

AltStyle によって変換されたページ (->オリジナル) /