DAG DP patterns
Dynamic programming on a DAG: the topological order is the order of the states, and the same loop counts paths, finds the heaviest or cheapest path, and counts reachable vertices.
Learning goals
- Treat vertices as DP states and a topological order as the evaluation order
- Count paths, with and without a modulus
- Find the heaviest path anywhere and the cheapest path between two vertices
- Count how many vertices each vertex can reach
Prerequisites
Dynamic programming needs states whose dependencies form no cycle. A DAG is exactly that: the vertices are the states and the arrows are the dependencies, so the topological order tells you what to compute first.
Topological state order
In a DP over a DAG, the value of a vertex depends on its predecessors (a *forward* DP) or on its successors (a *backward* DP). A topological order lists vertices so that every predecessor comes first; evaluate forward DPs in that order and backward DPs in the reverse order. Either way, when a vertex is computed everything it needs is ready.
The code counts the paths from every vertex **to** a target t: a backward DP over the reverse topological order, ways[u] = sum of ways[v] over the arrows u → v. Without a topological order the same recurrence would need memoised recursion.
1static long[] pathsToTarget(int n, int[][] edges, int t) { // ways[u] = number of paths from u to t in a DAG2int[] order = topoOrder(n, edges);3long[] ways = new long[n];4ways[t] = 1; // the empty path5for (int i = n - 1; i >= 0; i--) { // successors first: reverse topological order6int u = order[i];7for (int[] e : edges) if (e[0] == u) ways[u] += ways[e[1]];8}9return ways;10}1112static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG13int[] indeg = new int[n], order = new int[n];14for (int[] e : edges) indeg[e[1]]++;15Deque<Integer> q = new ArrayDeque<>();16for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);17int k = 0;18while (!q.isEmpty()) {19int u = q.poll();20order[k++] = u;21for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);22}23return order;24}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Number of paths
To count the paths from s to v, add up the counts of all predecessors: ways[v] = sum of ways[u] over arrows u → v, with ways[s] = 1. Parallel arrows count separately, since they are different paths. The number of paths can grow exponentially, so answers are often asked modulo a prime such as 1_000_000_007.
Take the remainder after each addition to avoid overflow. The *Count the paths* option of the DAG animation shows the counts building up along the topological order.
1static long countPathsModulo(int n, int[][] edges, int s, int t, long mod) { // paths s -> t in a DAG, modulo mod2int[] order = topoOrder(n, edges);3long[] ways = new long[n];4ways[s] = 1 % mod;5for (int u : order)6for (int[] e : edges)7if (e[0] == u) ways[e[1]] = (ways[e[1]] + ways[u]) % mod; // every path to u extends along this arrow8return ways[t];9}1011static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG12int[] indeg = new int[n], order = new int[n];13for (int[] e : edges) indeg[e[1]]++;14Deque<Integer> q = new ArrayDeque<>();15for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);16int k = 0;17while (!q.isEmpty()) {18int u = q.poll();19order[k++] = u;20for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);21}22return order;23}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Maximum path weight
The heaviest path **anywhere** in a weighted DAG (it may start and end at any vertices) uses best[v] = the heaviest path ending at v. A path may start at v itself, so best[v] = max(0, best[u] + w) over arrows u → v. The answer is the largest best[v]. With negative weights the zero lets a path restart where it would otherwise lose weight.
This is the DAG version of the maximum-subarray idea (Kadane's algorithm). The same pass also gives project critical paths when the weights are task durations.
1static long maxPathWeight(int n, int[][] edges) { // heaviest path with at least one vertex; 0 for a single vertex2int[] order = topoOrder(n, edges);3long[] best = new long[n]; // heaviest path that ends at v4long answer = 0;5for (int u : order) {6answer = Math.max(answer, best[u]);7for (int[] e : edges)8if (e[0] == u) best[e[1]] = Math.max(best[e[1]], best[u] + e[2]); // extend, or keep a fresh start at 09}10return answer;11}1213static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG14int[] indeg = new int[n], order = new int[n];15for (int[] e : edges) indeg[e[1]]++;16Deque<Integer> q = new ArrayDeque<>();17for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);18int k = 0;19while (!q.isEmpty()) {20int u = q.poll();21order[k++] = u;22for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);23}24return order;25}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Minimum cost path
For the cheapest path from s to t, set cost[s] = 0 and relax each arrow in topological order: cost[v] = min(cost[v], cost[u] + w). Vertices not reachable from s keep "unreachable". Because the order guarantees every arrow into v was tried before v is used, **negative weights** are allowed, and the work is O(n + m).
Compare with Dijkstra, which needs non-negative weights and a heap. The *Shortest path in a DAG* preset in the animation shows vertices before the source staying unreachable.
1static long dagMinCost(int n, int[][] edges, int s, int t) { // Long.MAX_VALUE when t is unreachable; weights may be negative2int[] order = topoOrder(n, edges);3long[] cost = new long[n];4Arrays.fill(cost, Long.MAX_VALUE);5cost[s] = 0;6for (int u : order) {7if (cost[u] == Long.MAX_VALUE) continue;8for (int[] e : edges)9if (e[0] == u && cost[u] + e[2] < cost[e[1]]) cost[e[1]] = cost[u] + e[2];10}11return cost[t];12}1314static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG15int[] indeg = new int[n], order = new int[n];16for (int[] e : edges) indeg[e[1]]++;17Deque<Integer> q = new ArrayDeque<>();18for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);19int k = 0;20while (!q.isEmpty()) {21int u = q.poll();22order[k++] = u;23for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);24}25return order;26}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Reachability counts
How many vertices can each vertex reach? Counting paths would count a vertex several times if it is reachable in several ways, so use sets: reach[u] = {u} united with reach[v] for every successor v, computed in reverse topological order. With one BitSet per vertex each union is a few word operations; the answer is the cardinality.
Storing all sets takes O(n²) bits, which is fine for thousands of vertices. For larger graphs the exact count is hard, and approximate sketches are used. The *Descendants* option in the facts animation shows one such set.
1static int[] reachableCounts(int n, int[][] edges) { // counts[u] = vertices reachable from u, u included; edges form a DAG2int[] order = topoOrder(n, edges);3BitSet[] reach = new BitSet[n];4int[] counts = new int[n];5for (int i = n - 1; i >= 0; i--) {6int u = order[i];7reach[u] = new BitSet(n);8reach[u].set(u);9for (int[] e : edges) if (e[0] == u) reach[u].or(reach[e[1]]); // the successors are finished already10counts[u] = reach[u].cardinality();11}12return counts;13}1415static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG16int[] indeg = new int[n], order = new int[n];17for (int[] e : edges) indeg[e[1]]++;18Deque<Integer> q = new ArrayDeque<>();19for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);20int k = 0;21while (!q.isEmpty()) {22int u = q.poll();23order[k++] = u;24for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);25}26return order;27}
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
- All Paths From Source to Target
Medium
- Number of Ways to Arrive at Destination
- Parallel Courses III
- Longest Increasing Path in a Matrix
Advanced
- Largest Color Value in a Directed Graph
Key takeaways
- On a DAG the states are the vertices and the topological order is the evaluation order.
- Forward DP follows the order, backward DP follows its reverse.
- Counting paths adds predecessor counts, usually modulo a prime.
- Cheapest paths on a DAG tolerate negative weights and need no heap; use sets or bitsets for reachability counts.