-
Notifications
You must be signed in to change notification settings - Fork 20.1k
Add DFS with parent-completion constraint for DAG traversal #6467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
DenizAltunkapan
merged 9 commits into
TheAlgorithms:master
from
StathisVeinoglou:graph-dfs-parent-constraint
Aug 15, 2025
+253
−0
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
bdd85a8
Add DFS with parent-completion constraint for DAG traversal
StathisVeinoglou 7193d96
warning in PartitionProblem.java affecting tests
StathisVeinoglou aa9c093
added clang-format and updated javadoc
StathisVeinoglou cfdad19
optimized imports and rechecked camelCase format in tests
StathisVeinoglou 87d208e
removed .* import and made small visual change
StathisVeinoglou 0a990c6
replaced a inline return with correct {} block
StathisVeinoglou b113746
Removed changed in PartitionProblem.java, Renamed class name to be st…
StathisVeinoglou f0fe5f7
updated for clang format
StathisVeinoglou ec54953
Merge branch 'master' into graph-dfs-parent-constraint
DenizAltunkapan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
src/main/java/com/thealgorithms/graph/GraphTraversal.java
DenizAltunkapan marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package com.thealgorithms.graph; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
||
/** | ||
* DFS that visits a successor only when all its predecessors are already visited, | ||
* emitting VISIT and SKIP events. | ||
* <p> | ||
* This class includes a DFS variant that visits a successor only when all of its | ||
* predecessors have already been visited | ||
* </p> | ||
* <p>Related reading: | ||
* <ul> | ||
* <li><a href="https://en.wikipedia.org/wiki/Topological_sorting">Topological sorting</a></li> | ||
* <li><a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a></li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
|
||
public final class GraphTraversal { | ||
|
||
private GraphTraversal() { | ||
// utility class | ||
} | ||
|
||
/** An event emitted by the traversal: either a VISIT with an order, or a SKIP with a note. */ | ||
public static final class TraversalEvent<T> { | ||
DenizAltunkapan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private final T node; | ||
private final Integer order; // non-null for visit, null for skip | ||
private final String note; // non-null for skip, null for visit | ||
|
||
private TraversalEvent(T node, Integer order, String note) { | ||
this.node = node; | ||
this.order = order; | ||
this.note = note; | ||
} | ||
|
||
/** A visit event with an increasing order (0,1,2,...) */ | ||
public static <T> TraversalEvent<T> visit(T node, int order) { | ||
return new TraversalEvent<>(Objects.requireNonNull(node), order, null); | ||
} | ||
|
||
/** A skip event with an explanatory note (e.g., not all parents visited yet). */ | ||
public static <T> TraversalEvent<T> skip(T node, String note) { | ||
return new TraversalEvent<>(Objects.requireNonNull(node), null, Objects.requireNonNull(note)); | ||
} | ||
|
||
public boolean isVisit() { | ||
return order != null; | ||
} | ||
public boolean isSkip() { | ||
return order == null; | ||
} | ||
public T node() { | ||
return node; | ||
} | ||
public Integer order() { | ||
return order; | ||
} | ||
public String note() { | ||
return note; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return isVisit() ? "VISIT(" + node + ", order=" + order + ")" : "SKIP(" + node + ", " + note + ")"; | ||
} | ||
} | ||
|
||
/** | ||
* DFS (recursive) that records the order of first visit starting at {@code start}, | ||
* but only recurses to a child when <b>all</b> its predecessors have been visited. | ||
* If a child is encountered early (some parent unvisited), a SKIP event is recorded. | ||
* | ||
* <p>Equivalent idea to the Python pseudo in the user's description (with successors and predecessors), | ||
* but implemented in Java and returning a sequence of {@link TraversalEvent}s.</p> | ||
* | ||
* @param successors adjacency list: for each node, its outgoing neighbors | ||
* @param start start node | ||
* @return immutable list of traversal events (VISITs with monotonically increasing order and SKIPs with messages) | ||
* @throws IllegalArgumentException if {@code successors} is null | ||
*/ | ||
public static <T> List<TraversalEvent<T>> dfsRecursiveOrder(Map<T, List<T>> successors, T start) { | ||
if (successors == null) { | ||
throw new IllegalArgumentException("successors must not be null"); | ||
} | ||
// derive predecessors once | ||
Map<T, List<T>> predecessors = derivePredecessors(successors); | ||
return dfsRecursiveOrder(successors, predecessors, start); | ||
} | ||
|
||
/** | ||
* Same as {@link #dfsRecursiveOrder(Map, Object)} but with an explicit predecessors map. | ||
*/ | ||
public static <T> List<TraversalEvent<T>> dfsRecursiveOrder(Map<T, List<T>> successors, Map<T, List<T>> predecessors, T start) { | ||
|
||
if (successors == null || predecessors == null) { | ||
throw new IllegalArgumentException("successors and predecessors must not be null"); | ||
} | ||
if (start == null) { | ||
return List.of(); | ||
} | ||
if (!successors.containsKey(start) && !appearsAnywhere(successors, start)) { | ||
return List.of(); // start not present in graph | ||
} | ||
|
||
List<TraversalEvent<T>> events = new ArrayList<>(); | ||
Set<T> visited = new HashSet<>(); | ||
int[] order = {0}; | ||
dfs(start, successors, predecessors, visited, order, events); | ||
return Collections.unmodifiableList(events); | ||
} | ||
|
||
private static <T> void dfs(T u, Map<T, List<T>> succ, Map<T, List<T>> pred, Set<T> visited, int[] order, List<TraversalEvent<T>> out) { | ||
DenizAltunkapan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (!visited.add(u)) { | ||
return; // already visited | ||
} | ||
out.add(TraversalEvent.visit(u, order[0]++)); // record visit and increment | ||
|
||
for (T v : succ.getOrDefault(u, List.of())) { | ||
if (visited.contains(v)) { | ||
continue; | ||
} | ||
if (allParentsVisited(v, visited, pred)) { | ||
dfs(v, succ, pred, visited, order, out); | ||
} else { | ||
out.add(TraversalEvent.skip(v, "⛔ Skipping " + v + ": not all parents are visited yet.")); | ||
// do not mark visited; it may be visited later from another parent | ||
} | ||
} | ||
} | ||
|
||
private static <T> boolean allParentsVisited(T node, Set<T> visited, Map<T, List<T>> pred) { | ||
for (T p : pred.getOrDefault(node, List.of())) { | ||
if (!visited.contains(p)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
private static <T> boolean appearsAnywhere(Map<T, List<T>> succ, T node) { | ||
DenizAltunkapan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (succ.containsKey(node)) { | ||
return true; | ||
} | ||
for (List<T> nbrs : succ.values()) { | ||
if (nbrs != null && nbrs.contains(node)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private static <T> Map<T, List<T>> derivePredecessors(Map<T, List<T>> succ) { | ||
Map<T, List<T>> pred = new HashMap<>(); | ||
// ensure keys exist for all nodes appearing anywhere | ||
for (Map.Entry<T, List<T>> e : succ.entrySet()) { | ||
pred.computeIfAbsent(e.getKey(), k -> new ArrayList<>()); | ||
for (T v : e.getValue()) { | ||
pred.computeIfAbsent(v, k -> new ArrayList<>()).add(e.getKey()); | ||
} | ||
} | ||
return pred; | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
src/test/java/com/thealgorithms/graph/GraphTraversalTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.thealgorithms.graph; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
||
import com.thealgorithms.graph.GraphTraversal.TraversalEvent; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import org.junit.jupiter.api.Test; | ||
|
||
class GraphTraversalTest { | ||
|
||
// A -> B, A -> C, B -> D, C -> D (classic diamond) | ||
private static Map<String, List<String>> diamond() { | ||
Map<String, List<String>> g = new LinkedHashMap<>(); | ||
g.put("A", List.of("B", "C")); | ||
g.put("B", List.of("D")); | ||
g.put("C", List.of("D")); | ||
g.put("D", List.of()); | ||
return g; | ||
} | ||
|
||
@Test | ||
void dfsRecursiveOrderEmitsSkipUntilAllParentsVisited() { | ||
List<TraversalEvent<String>> events = GraphTraversal.dfsRecursiveOrder(diamond(), "A"); | ||
|
||
// Expect visits in order and a skip for first time we meet D (via B) before C is visited. | ||
var visits = events.stream().filter(TraversalEvent::isVisit).toList(); | ||
var skips = events.stream().filter(TraversalEvent::isSkip).toList(); | ||
|
||
// Visits should be A(0), B(1), C(2), D(3) in some deterministic order given adjacency | ||
assertThat(visits).hasSize(4); | ||
assertThat(visits.get(0).node()).isEqualTo("A"); | ||
assertThat(visits.get(0).order()).isEqualTo(0); | ||
assertThat(visits.get(1).node()).isEqualTo("B"); | ||
assertThat(visits.get(1).order()).isEqualTo(1); | ||
assertThat(visits.get(2).node()).isEqualTo("C"); | ||
assertThat(visits.get(2).order()).isEqualTo(2); | ||
assertThat(visits.get(3).node()).isEqualTo("D"); | ||
assertThat(visits.get(3).order()).isEqualTo(3); | ||
|
||
// One skip when we first encountered D from B (before C was visited) | ||
assertThat(skips).hasSize(1); | ||
assertThat(skips.get(0).node()).isEqualTo("D"); | ||
assertThat(skips.get(0).note()).contains("not all parents"); | ||
} | ||
|
||
@Test | ||
void returnsEmptyWhenStartNotInGraph() { | ||
Map<Integer, List<Integer>> g = Map.of(1, List.of(2), 2, List.of(1)); | ||
assertThat(GraphTraversal.dfsRecursiveOrder(g, 99)).isEmpty(); | ||
} | ||
|
||
@Test | ||
void nullSuccessorsThrows() { | ||
assertThrows(IllegalArgumentException.class, () -> GraphTraversal.dfsRecursiveOrder(null, "A")); | ||
} | ||
|
||
@Test | ||
void worksWithExplicitPredecessors() { | ||
Map<Integer, List<Integer>> succ = new HashMap<>(); | ||
succ.put(10, List.of(20)); | ||
succ.put(20, List.of(30)); | ||
succ.put(30, List.of()); | ||
|
||
Map<Integer, List<Integer>> pred = new HashMap<>(); | ||
pred.put(10, List.of()); | ||
pred.put(20, List.of(10)); | ||
pred.put(30, List.of(20)); | ||
|
||
var events = GraphTraversal.dfsRecursiveOrder(succ, pred, 10); | ||
var visitNodes = events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node).toList(); | ||
assertThat(visitNodes).containsExactly(10, 20, 30); | ||
} | ||
|
||
@Test | ||
void cycleProducesSkipsButNoInfiniteRecursion() { | ||
Map<String, List<String>> succ = new LinkedHashMap<>(); | ||
succ.put("X", List.of("Y")); | ||
succ.put("Y", List.of("X")); // 2-cycle | ||
|
||
var events = GraphTraversal.dfsRecursiveOrder(succ, "X"); | ||
// Only X is visited; encountering Y from X causes skip because Y's parent X is visited, | ||
// but when recursing to Y we'd hit back to X (already visited) and stop; no infinite loop. | ||
assertThat(events.stream().anyMatch(TraversalEvent::isVisit)).isTrue(); | ||
assertThat(events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node)).contains("X"); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.