Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* @author Md Asif Joardar
*
* Description: The partition problem is a classic problem in computer science
Expand Down
173 changes: 173 additions & 0 deletions src/main/java/com/thealgorithms/graph/GraphTraversal.java
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> {
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) {

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) {
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 src/test/java/com/thealgorithms/graph/GraphTraversalTest.java
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");
}
}