Directed reachability
Reachability in a directed graph is asymmetric: ancestors reach a vertex, descendants are reached from it, the transitive closure records every pair, and reversing the graph answers the ancestor questions with one search.
Learning goals
- Define ancestors and descendants and compute each with one search
- Build the transitive closure of a DAG in reverse topological order
- Answer a question about many targets with one search of the reversed graph
Prerequisites
Which modules does this one depend on, directly or not? Which others are affected if it changes? Which states can still reach a safe end? These questions all ask for the set of vertices on one side of a path.
Ancestor queries
An **ancestor** of v is a vertex that has a path to v. To list the ancestors of every vertex of a DAG, run one reverse search per vertex, or propagate sets along a topological order. Ancestors of v are its direct predecessors plus all of *their* ancestors.
The answers can be large (up to n(n - 1) / 2 pairs in total), so building them all costs O(n(n + m)) time and O(n^2) space. For a single vertex one search is enough. Choose *Ancestors of a vertex* in the animation.
1static List<List<Integer>> allAncestors(int n, int[][] edges) { // edges {u, v}: ancestors.get(v) lists the vertices with a path to v2List<List<Integer>> rev = new ArrayList<>();3for (int i = 0; i < n; i++) rev.add(new ArrayList<>());4for (int[] e : edges) rev.get(e[1]).add(e[0]);5List<List<Integer>> result = new ArrayList<>();6for (int v = 0; v < n; v++) {7boolean[] seen = new boolean[n];8Deque<Integer> stack = new ArrayDeque<>(rev.get(v));9while (!stack.isEmpty()) {10int u = stack.pop();11if (seen[u]) continue;12seen[u] = true;13for (int p : rev.get(u)) stack.push(p);14}15List<Integer> list = new ArrayList<>();16for (int u = 0; u < n; u++) if (seen[u]) list.add(u);17result.add(list);18}19return result;20}
Directed graph facts: degrees, sources, sinks, descendants and ancestors
Interactive · edit the inputdirected graph · 6 vertices · 6 edges
in-degree
out-degree
Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b.
1int[][] degrees(List<List<Integer>> g) { // {in-degree, out-degree} per vertex2int n = g.size(); int[] in = new int[n], out = new int[n];3for (int u = 0; u < n; u++) for (int v : g.get(u)) { out[u]++; in[v]++; }4return new int[][]{in, out}; // source: in == 0, sink: out == 05}6List<Integer> descendants(List<List<Integer>> g, int s) { // reached by one or more arrows7boolean[] seen = new boolean[g.size()];8Deque<Integer> stack = new ArrayDeque<>(g.get(s));9List<Integer> found = new ArrayList<>();10while (!stack.isEmpty()) {11int u = stack.pop();12if (seen[u]) continue;13seen[u] = true; found.add(u);14for (int v : g.get(u)) stack.push(v);15}16return found; // contains s only when s lies on a cycle17}18// ancestors(g, s) = descendants(reverse(g), s)
Live state
n
6
arrows
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 arrows=6 | Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b. |
| 2 | L3 | arrow=0 → 1 out[a]=1 in[b]=1 | Arrow 0 → 1: out-degree of 0 is now 1, in-degree of 1 is now 1. |
| 3 | L3 | arrow=0 → 2 out[a]=2 in[b]=1 | Arrow 0 → 2: out-degree of 0 is now 2, in-degree of 2 is now 1. |
| 4 | L3 | arrow=1 → 3 out[a]=1 in[b]=1 | Arrow 1 → 3: out-degree of 1 is now 1, in-degree of 3 is now 1. |
| 5 | L3 | arrow=2 → 3 out[a]=1 in[b]=2 | Arrow 2 → 3: out-degree of 2 is now 1, in-degree of 3 is now 2. |
| 6 | L3 | arrow=2 → 4 out[a]=2 in[b]=1 | Arrow 2 → 4: out-degree of 2 is now 2, in-degree of 4 is now 1. |
| 7 | L3 | arrow=3 → 5 out[a]=1 in[b]=1 | Arrow 3 → 5: out-degree of 3 is now 1, in-degree of 5 is now 1. |
| 8 | L4 | return=in [0, 1, 1, 2, 1, 1] out [2, 1, 2, 1, 0, 0] sources [0] sinks [4, 5] | Sources (in-degree 0): [0]. Sinks (out-degree 0): [4, 5]. The in-degrees add up to 6, the out-degrees to 6: both equal the 6 arrows. |
Descendants
A **descendant** of v is a vertex reachable from v by one or more arrows. One DFS or BFS from v finds them all. In a graph with cycles, v can be its own descendant (if it lies on a cycle); in a DAG it never is, which is another test for acyclicity.
Descendants are what a change affects: the modules to rebuild, the cells to recompute. The animation's second option, *Descendants of a vertex*, includes a preset where the start vertex ends up in its own list.
1static List<Integer> descendantSet(List<List<Integer>> g, int s) { // reached by one or more arrows, in increasing order2boolean[] seen = new boolean[g.size()];3Deque<Integer> stack = new ArrayDeque<>(g.get(s));4while (!stack.isEmpty()) {5int u = stack.pop();6if (seen[u]) continue;7seen[u] = true;8for (int v : g.get(u)) stack.push(v);9}10List<Integer> out = new ArrayList<>();11for (int v = 0; v < g.size(); v++) if (seen[v]) out.add(v); // contains s only when s lies on a cycle12return out;13}
Directed graph facts: degrees, sources, sinks, descendants and ancestors
Interactive · edit the inputdirected graph · 6 vertices · 6 edges
in-degree
out-degree
Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b.
1int[][] degrees(List<List<Integer>> g) { // {in-degree, out-degree} per vertex2int n = g.size(); int[] in = new int[n], out = new int[n];3for (int u = 0; u < n; u++) for (int v : g.get(u)) { out[u]++; in[v]++; }4return new int[][]{in, out}; // source: in == 0, sink: out == 05}6List<Integer> descendants(List<List<Integer>> g, int s) { // reached by one or more arrows7boolean[] seen = new boolean[g.size()];8Deque<Integer> stack = new ArrayDeque<>(g.get(s));9List<Integer> found = new ArrayList<>();10while (!stack.isEmpty()) {11int u = stack.pop();12if (seen[u]) continue;13seen[u] = true; found.add(u);14for (int v : g.get(u)) stack.push(v);15}16return found; // contains s only when s lies on a cycle17}18// ancestors(g, s) = descendants(reverse(g), s)
Live state
n
6
arrows
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 arrows=6 | Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b. |
| 2 | L3 | arrow=0 → 1 out[a]=1 in[b]=1 | Arrow 0 → 1: out-degree of 0 is now 1, in-degree of 1 is now 1. |
| 3 | L3 | arrow=0 → 2 out[a]=2 in[b]=1 | Arrow 0 → 2: out-degree of 0 is now 2, in-degree of 2 is now 1. |
| 4 | L3 | arrow=1 → 3 out[a]=1 in[b]=1 | Arrow 1 → 3: out-degree of 1 is now 1, in-degree of 3 is now 1. |
| 5 | L3 | arrow=2 → 3 out[a]=1 in[b]=2 | Arrow 2 → 3: out-degree of 2 is now 1, in-degree of 3 is now 2. |
| 6 | L3 | arrow=2 → 4 out[a]=2 in[b]=1 | Arrow 2 → 4: out-degree of 2 is now 2, in-degree of 4 is now 1. |
| 7 | L3 | arrow=3 → 5 out[a]=1 in[b]=1 | Arrow 3 → 5: out-degree of 3 is now 1, in-degree of 5 is now 1. |
| 8 | L4 | return=in [0, 1, 1, 2, 1, 1] out [2, 1, 2, 1, 0, 0] sources [0] sinks [4, 5] | Sources (in-degree 0): [0]. Sinks (out-degree 0): [4, 5]. The in-degrees add up to 6, the out-degrees to 6: both equal the 6 arrows. |
Transitive closure
The **transitive closure** lists every pair (u, v) where v is reachable from u. On a DAG it can be built without any search per vertex: process vertices in *reverse* topological order and set reach[u] = {u} united with reach[v] for every arrow u → v. Each reach[v] is complete before u needs it.
Storing each set as a bitset makes every union a handful of word operations. The animation for graph reachability shows the closure by Warshall's algorithm, which also works when the graph has cycles.
1static BitSet[] dagClosure(List<List<Integer>> g) { // reach[u] holds every vertex reachable from u, u included; g must be a DAG2int n = g.size();3int[] indeg = new int[n], order = new int[n];4for (List<Integer> out : g) for (int v : out) indeg[v]++;5Deque<Integer> q = new ArrayDeque<>();6for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);7int k = 0;8while (!q.isEmpty()) {9int u = q.poll();10order[k++] = u;11for (int v : g.get(u)) if (--indeg[v] == 0) q.add(v);12}13BitSet[] reach = new BitSet[n];14for (int i = n - 1; i >= 0; i--) { // reverse topological order: successors are finished first15int u = order[i];16reach[u] = new BitSet(n);17reach[u].set(u);18for (int v : g.get(u)) reach[u].or(reach[v]);19}20return reach;21}
Reachability: path existence, reversed search, transitive closure
Interactive · edit the inputdirected graph · 6 vertices · 5 edges
stack
Is there a path from 0 to 3? Search from 0 and stop as soon as 3 is reached. Arrows can only be followed forwards.
1boolean reachable(List<List<Integer>> g, int s, int t) {2boolean[] seen = new boolean[g.size()];3Deque<Integer> stack = new ArrayDeque<>(); stack.push(s); seen[s] = true;4while (!stack.isEmpty()) {5int u = stack.pop();6if (u == t) return true;7for (int v : g.get(u)) if (!seen[v]) { seen[v] = true; stack.push(v); }8}9return false;10}11boolean[] canReach(List<List<Integer>> reversed, int t) { // search from t along reversed edges12boolean[] seen = new boolean[reversed.size()];13Deque<Integer> q = new ArrayDeque<>(); q.add(t); seen[t] = true;14while (!q.isEmpty()) { int u = q.poll(); for (int v : reversed.get(u)) if (!seen[v]) { seen[v] = true; q.add(v); } }15return seen;16}17boolean[][] closure(boolean[][] adj) { // Warshall: reach[i][j] using intermediate vertices 0..k18int n = adj.length; boolean[][] r = new boolean[n][];19for (int i = 0; i < n; i++) { r[i] = adj[i].clone(); r[i][i] = true; }20for (int k = 0; k < n; k++)21for (int i = 0; i < n; i++)22if (r[i][k]) for (int j = 0; j < n; j++) r[i][j] |= r[k][j];23return r;24}
Live state
s
0
t
3
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | s=0 t=3 | Is there a path from 0 to 3? Search from 0 and stop as soon as 3 is reached. Arrows can only be followed forwards. |
| 2 | L7 | u=0 pushed=[1, 4] | Pop 0: push the unvisited out-neighbours 1, 4. |
| 3 | L7 | u=4 pushed=none | Pop 4: no unvisited out-neighbour. |
| 4 | L7 | u=1 pushed=[2] | Pop 1: push the unvisited out-neighbours 2. |
| 5 | L7 | u=2 pushed=[3] | Pop 2: push the unvisited out-neighbours 3. |
| 6 | L6 | return=true | 3 reached: a path from 0 exists. |
Reversed DAG
To find all vertices that can reach *any* of several targets, do not search from each vertex. Reverse the graph and start one search from all the targets together; everything it reaches is a vertex with a path to a target. The cost is one O(n + m) search.
This solves "which states are safe?" problems (a state is safe if every path leads to a terminal), and "which vertices can influence this set?". It is the multi-source form of the reversed-reachability idea from the previous chapter.
1static boolean[] canReachTargets(List<List<Integer>> g, int[] targets) { // true for every vertex with a path to some target2int n = g.size();3List<List<Integer>> rev = new ArrayList<>();4for (int i = 0; i < n; i++) rev.add(new ArrayList<>());5for (int u = 0; u < n; u++) for (int v : g.get(u)) rev.get(v).add(u);6boolean[] can = new boolean[n];7Deque<Integer> stack = new ArrayDeque<>();8for (int t : targets) if (!can[t]) { can[t] = true; stack.push(t); }9while (!stack.isEmpty()) {10int u = stack.pop();11for (int p : rev.get(u)) if (!can[p]) { can[p] = true; stack.push(p); }12}13return can;14}
Directed graph facts: degrees, sources, sinks, descendants and ancestors
Interactive · edit the inputdirected graph · 6 vertices · 6 edges
in-degree
out-degree
Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b.
1int[][] degrees(List<List<Integer>> g) { // {in-degree, out-degree} per vertex2int n = g.size(); int[] in = new int[n], out = new int[n];3for (int u = 0; u < n; u++) for (int v : g.get(u)) { out[u]++; in[v]++; }4return new int[][]{in, out}; // source: in == 0, sink: out == 05}6List<Integer> descendants(List<List<Integer>> g, int s) { // reached by one or more arrows7boolean[] seen = new boolean[g.size()];8Deque<Integer> stack = new ArrayDeque<>(g.get(s));9List<Integer> found = new ArrayList<>();10while (!stack.isEmpty()) {11int u = stack.pop();12if (seen[u]) continue;13seen[u] = true; found.add(u);14for (int v : g.get(u)) stack.push(v);15}16return found; // contains s only when s lies on a cycle17}18// ancestors(g, s) = descendants(reverse(g), s)
Live state
n
6
arrows
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 arrows=6 | Every vertex starts with in-degree 0 and out-degree 0. Each arrow a → b adds 1 to the out-degree of a and 1 to the in-degree of b. |
| 2 | L3 | arrow=0 → 1 out[a]=1 in[b]=1 | Arrow 0 → 1: out-degree of 0 is now 1, in-degree of 1 is now 1. |
| 3 | L3 | arrow=0 → 2 out[a]=2 in[b]=1 | Arrow 0 → 2: out-degree of 0 is now 2, in-degree of 2 is now 1. |
| 4 | L3 | arrow=1 → 3 out[a]=1 in[b]=1 | Arrow 1 → 3: out-degree of 1 is now 1, in-degree of 3 is now 1. |
| 5 | L3 | arrow=2 → 3 out[a]=1 in[b]=2 | Arrow 2 → 3: out-degree of 2 is now 1, in-degree of 3 is now 2. |
| 6 | L3 | arrow=2 → 4 out[a]=2 in[b]=1 | Arrow 2 → 4: out-degree of 2 is now 2, in-degree of 4 is now 1. |
| 7 | L3 | arrow=3 → 5 out[a]=1 in[b]=1 | Arrow 3 → 5: out-degree of 3 is now 1, in-degree of 5 is now 1. |
| 8 | L4 | return=in [0, 1, 1, 2, 1, 1] out [2, 1, 2, 1, 0, 0] sources [0] sinks [4, 5] | Sources (in-degree 0): [0]. Sinks (out-degree 0): [4, 5]. The in-degrees add up to 6, the out-degrees to 6: both equal the 6 arrows. |
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- All Ancestors of a Node in a Directed Acyclic Graph
- Find Eventual Safe States
- Course Schedule IV
Advanced
- Minimum Number of Vertices to Reach All Nodes
Key takeaways
- Ancestors reach a vertex; descendants are reached from it; both come from one search, the second on the reversed graph.
- A vertex is its own descendant only when it lies on a cycle.
- On a DAG, build the closure in reverse topological order with bitsets.
- Search the reversed graph from all targets at once to find everything that can reach them.