Multi-state graph DP
When a constraint such as a number of stops, a number of colours or a remaining budget matters, the state becomes (vertex, counter); the counter turns an arbitrary graph with cycles into layers that form a DAG.
Learning goals
- Add a counter to the state and describe the layers
- Count walks of exactly j edges and cheapest walks of at most k edges
- Handle a resource budget and a colour limit
Prerequisites
A route with at most two stops, a path that may touch at most one dangerous room, a trip with a fuel limit: the vertex alone is not enough to describe where you are. Add what else matters to the state and the problem becomes a plain DP again.
Color-count paths
Colour the vertices; suppose odd-numbered ones are *red* and a walk may visit at most c red vertices. The state is (vertex, reds used so far). Entering a red vertex moves the count from r to r + 1; moves that would exceed c are dropped. Counting walks of exactly L edges is then a table of L + 1 layers.
Even if the graph has cycles, the layers do not: each step goes from layer j to layer j + 1. Choose the fourth option of the layered DP animation; the counts per number of reds appear in every vertex label.
1static long countColourPaths(int n, int[][] edges, int s, int t, boolean[] red, int maxReds, int length) { // walks with exactly length edges2long[][] cur = new long[n][maxReds + 1]; // cur[v][r]: walks ending at v with r red visits3int r0 = red[s] ? 1 : 0;4if (r0 <= maxReds) cur[s][r0] = 1;5for (int j = 1; j <= length; j++) {6long[][] next = new long[n][maxReds + 1];7for (int[] e : edges) {8int add = red[e[1]] ? 1 : 0;9for (int r = 0; r + add <= maxReds; r++) next[e[1]][r + add] += cur[e[0]][r];10}11cur = next;12}13long total = 0;14for (long x : cur[t]) total += x;15return total;16}
Multi-state graph DP: the state is (vertex, extra counter)
Interactive · edit the inputlayer 0 · 5 vertices · 8 edges
1
0
0
0
0
j = 0
W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length.
1static long[] walkCounts(int n, int[][] edges, int s, int t, int k) { // walks s -> t with exactly j edges, j = 0..k2long[] cur = new long[n];3cur[s] = 1;4long[] out = new long[k + 1];5out[0] = cur[t];6for (int j = 1; j <= k; j++) {7long[] next = new long[n];8for (int[] e : edges) next[e[1]] += cur[e[0]]; // every walk with j - 1 edges extends along every arrow9cur = next;10out[j] = cur[t];11}12return out;13}14static long cheapestAtMost(int n, int[][] edges, int s, int t, int k) { // Long.MAX_VALUE when t needs more than k edges15long INF = Long.MAX_VALUE / 2;16long[] dist = new long[n];17Arrays.fill(dist, INF);18dist[s] = 0;19for (int j = 1; j <= k; j++) {20long[] next = dist.clone(); // keeping the old value means "at most j edges"21for (int[] e : edges) if (dist[e[0]] < INF && dist[e[0]] + e[2] < next[e[1]]) next[e[1]] = dist[e[0]] + e[2];22dist = next;23}24return dist[t] >= INF ? Long.MAX_VALUE : dist[t];25}26static int fewestEdgesWithinBudget(int n, int[][] edges, int s, int t, int budget) { // weights >= 1; -1 if impossible27int INF = Integer.MAX_VALUE / 2;28int[][] f = new int[budget + 1][n]; // f[b][v]: fewest edges to reach v spending at most b29for (int[] row : f) Arrays.fill(row, INF);30f[0][s] = 0;31for (int b = 0; b <= budget; b++) {32if (b > 0) f[b] = f[b - 1].clone();33for (int[] e : edges)34if (e[2] <= b && f[b - e[2]][e[0]] < INF) f[b][e[1]] = Math.min(f[b][e[1]], f[b - e[2]][e[0]] + 1);35}36return f[budget][t] >= INF ? -1 : f[budget][t];37}38static long walksWithFewReds(int n, int[][] edges, int s, int t, int length, int maxReds) { // odd vertices are red; every visit counts39long[][] cur = new long[n][maxReds + 1];40int r0 = s % 2;41if (r0 <= maxReds) cur[s][r0] = 1;42for (int j = 1; j <= length; j++) {43long[][] next = new long[n][maxReds + 1];44for (int[] e : edges) {45int red = e[1] % 2;46for (int r = 0; r + red <= maxReds; r++) next[e[1]][r + red] += cur[e[0]][r];47}48cur = next;49}50long total = 0;51for (long x : cur[t]) total += x;52return total;53}
Live state
source
0
target
4
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | source=0 target=4 | W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length. |
| 2 | L8 | j=1 W[1][4]=0 | Layer 1: 0 walks of exactly 1 edge from 0 to 4. |
| 3 | L8 | j=2 W[2][4]=1 | Layer 2: 1 walk of exactly 2 edges from 0 to 4. |
| 4 | L8 | j=3 W[3][4]=3 | Layer 3: 3 walks of exactly 3 edges from 0 to 4. |
| 5 | L12 | return=[0, 0, 1, 3] | Walks from 0 to 4 by length 0..3: [0, 0, 1, 3]. Each layer needs one pass over the edges: O(k · m) in total. |
Paths with limited stops
*Cheapest flight with at most k stops* limits the number of edges to k + 1. Let cost[j][v] be the cheapest way to reach v using at most j edges. Layer j comes from layer j - 1 by relaxing every edge once: this is Bellman-Ford stopped after k + 1 rounds, which is why it works for graphs with cycles.
It is essential to relax from the **previous** layer's values, not the ones just updated in this round; otherwise a path could use more than j edges in one round. The code copies the array each layer. Compare Dijkstra, which ignores the stop count and can return a cheaper route that uses too many stops.
1static long cheapestWithStops(int n, int[][] flights, int s, int t, int maxStops) { // flights {from, to, price}; -1 when impossible2long INF = Long.MAX_VALUE / 2;3long[] cost = new long[n];4Arrays.fill(cost, INF);5cost[s] = 0;6for (int edgesUsed = 0; edgesUsed <= maxStops; edgesUsed++) { // at most maxStops + 1 edges7long[] next = cost.clone(); // relax from the previous layer only8for (int[] f : flights)9if (cost[f[0]] < INF && cost[f[0]] + f[2] < next[f[1]]) next[f[1]] = cost[f[0]] + f[2];10cost = next;11}12return cost[t] >= INF ? -1 : cost[t];13}
Multi-state graph DP: the state is (vertex, extra counter)
Interactive · edit the inputlayer 0 · 5 vertices · 8 edges
1
0
0
0
0
j = 0
W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length.
1static long[] walkCounts(int n, int[][] edges, int s, int t, int k) { // walks s -> t with exactly j edges, j = 0..k2long[] cur = new long[n];3cur[s] = 1;4long[] out = new long[k + 1];5out[0] = cur[t];6for (int j = 1; j <= k; j++) {7long[] next = new long[n];8for (int[] e : edges) next[e[1]] += cur[e[0]]; // every walk with j - 1 edges extends along every arrow9cur = next;10out[j] = cur[t];11}12return out;13}14static long cheapestAtMost(int n, int[][] edges, int s, int t, int k) { // Long.MAX_VALUE when t needs more than k edges15long INF = Long.MAX_VALUE / 2;16long[] dist = new long[n];17Arrays.fill(dist, INF);18dist[s] = 0;19for (int j = 1; j <= k; j++) {20long[] next = dist.clone(); // keeping the old value means "at most j edges"21for (int[] e : edges) if (dist[e[0]] < INF && dist[e[0]] + e[2] < next[e[1]]) next[e[1]] = dist[e[0]] + e[2];22dist = next;23}24return dist[t] >= INF ? Long.MAX_VALUE : dist[t];25}26static int fewestEdgesWithinBudget(int n, int[][] edges, int s, int t, int budget) { // weights >= 1; -1 if impossible27int INF = Integer.MAX_VALUE / 2;28int[][] f = new int[budget + 1][n]; // f[b][v]: fewest edges to reach v spending at most b29for (int[] row : f) Arrays.fill(row, INF);30f[0][s] = 0;31for (int b = 0; b <= budget; b++) {32if (b > 0) f[b] = f[b - 1].clone();33for (int[] e : edges)34if (e[2] <= b && f[b - e[2]][e[0]] < INF) f[b][e[1]] = Math.min(f[b][e[1]], f[b - e[2]][e[0]] + 1);35}36return f[budget][t] >= INF ? -1 : f[budget][t];37}38static long walksWithFewReds(int n, int[][] edges, int s, int t, int length, int maxReds) { // odd vertices are red; every visit counts39long[][] cur = new long[n][maxReds + 1];40int r0 = s % 2;41if (r0 <= maxReds) cur[s][r0] = 1;42for (int j = 1; j <= length; j++) {43long[][] next = new long[n][maxReds + 1];44for (int[] e : edges) {45int red = e[1] % 2;46for (int r = 0; r + red <= maxReds; r++) next[e[1]][r + red] += cur[e[0]][r];47}48cur = next;49}50long total = 0;51for (long x : cur[t]) total += x;52return total;53}
Live state
source
0
target
4
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | source=0 target=4 | W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length. |
| 2 | L8 | j=1 W[1][4]=0 | Layer 1: 0 walks of exactly 1 edge from 0 to 4. |
| 3 | L8 | j=2 W[2][4]=1 | Layer 2: 1 walk of exactly 2 edges from 0 to 4. |
| 4 | L8 | j=3 W[3][4]=3 | Layer 3: 3 walks of exactly 3 edges from 0 to 4. |
| 5 | L12 | return=[0, 0, 1, 3] | Walks from 0 to 4 by length 0..3: [0, 0, 1, 3]. Each layer needs one pass over the edges: O(k · m) in total. |
Edge count constraints
With a constraint on the **exact** number of edges, W[j][v] counts walks from the source to v with exactly j edges: W[j][v] = sum of W[j-1][u] over arrows u → v. This is the matrix power of the adjacency matrix in disguise: entry (s, v) of A^j. Parallel arrows and self-loops are counted as separate steps.
Walks, not paths, because vertices may repeat; the length bound keeps the count finite on cyclic graphs. The animation's first option prints one row per j.
1static long[] walkCounts(int n, int[][] edges, int s, int t, int k) { // walks s -> t with exactly j edges, j = 0..k2long[] cur = new long[n];3cur[s] = 1;4long[] out = new long[k + 1];5out[0] = cur[t];6for (int j = 1; j <= k; j++) {7long[] next = new long[n];8for (int[] e : edges) next[e[1]] += cur[e[0]]; // every walk with j - 1 edges extends along every arrow9cur = next;10out[j] = cur[t];11}12return out;13}
Multi-state graph DP: the state is (vertex, extra counter)
Interactive · edit the inputlayer 0 · 5 vertices · 8 edges
1
0
0
0
0
j = 0
W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length.
1static long[] walkCounts(int n, int[][] edges, int s, int t, int k) { // walks s -> t with exactly j edges, j = 0..k2long[] cur = new long[n];3cur[s] = 1;4long[] out = new long[k + 1];5out[0] = cur[t];6for (int j = 1; j <= k; j++) {7long[] next = new long[n];8for (int[] e : edges) next[e[1]] += cur[e[0]]; // every walk with j - 1 edges extends along every arrow9cur = next;10out[j] = cur[t];11}12return out;13}14static long cheapestAtMost(int n, int[][] edges, int s, int t, int k) { // Long.MAX_VALUE when t needs more than k edges15long INF = Long.MAX_VALUE / 2;16long[] dist = new long[n];17Arrays.fill(dist, INF);18dist[s] = 0;19for (int j = 1; j <= k; j++) {20long[] next = dist.clone(); // keeping the old value means "at most j edges"21for (int[] e : edges) if (dist[e[0]] < INF && dist[e[0]] + e[2] < next[e[1]]) next[e[1]] = dist[e[0]] + e[2];22dist = next;23}24return dist[t] >= INF ? Long.MAX_VALUE : dist[t];25}26static int fewestEdgesWithinBudget(int n, int[][] edges, int s, int t, int budget) { // weights >= 1; -1 if impossible27int INF = Integer.MAX_VALUE / 2;28int[][] f = new int[budget + 1][n]; // f[b][v]: fewest edges to reach v spending at most b29for (int[] row : f) Arrays.fill(row, INF);30f[0][s] = 0;31for (int b = 0; b <= budget; b++) {32if (b > 0) f[b] = f[b - 1].clone();33for (int[] e : edges)34if (e[2] <= b && f[b - e[2]][e[0]] < INF) f[b][e[1]] = Math.min(f[b][e[1]], f[b - e[2]][e[0]] + 1);35}36return f[budget][t] >= INF ? -1 : f[budget][t];37}38static long walksWithFewReds(int n, int[][] edges, int s, int t, int length, int maxReds) { // odd vertices are red; every visit counts39long[][] cur = new long[n][maxReds + 1];40int r0 = s % 2;41if (r0 <= maxReds) cur[s][r0] = 1;42for (int j = 1; j <= length; j++) {43long[][] next = new long[n][maxReds + 1];44for (int[] e : edges) {45int red = e[1] % 2;46for (int r = 0; r + red <= maxReds; r++) next[e[1]][r + red] += cur[e[0]][r];47}48cur = next;49}50long total = 0;51for (long x : cur[t]) total += x;52return total;53}
Live state
source
0
target
4
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | source=0 target=4 | W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length. |
| 2 | L8 | j=1 W[1][4]=0 | Layer 1: 0 walks of exactly 1 edge from 0 to 4. |
| 3 | L8 | j=2 W[2][4]=1 | Layer 2: 1 walk of exactly 2 edges from 0 to 4. |
| 4 | L8 | j=3 W[3][4]=3 | Layer 3: 3 walks of exactly 3 edges from 0 to 4. |
| 5 | L12 | return=[0, 0, 1, 3] | Walks from 0 to 4 by length 0..3: [0, 0, 1, 3]. Each layer needs one pass over the edges: O(k · m) in total. |
Resource-constrained paths
A **budget** works like a counter that is spent: the state is (vertex, amount spent), and the layers are the amounts. If every edge costs at least 1, an edge from layer b - w to layer b always goes forward, so the order of layers is a topological order. The question *fewest edges within total cost B* becomes f[b][v] = min(f[b-1][v], f[b-w][u] + 1).
When costs can be 0 or the budget is huge, plain DP over amounts is too slow or ill-founded, and the problem needs Dijkstra on the states instead. Here the budget is small. Try the *Budget too small* preset to see an answer of -1.
1static int fewestEdgesWithinBudget(int n, int[][] edges, int s, int t, int budget) { // edges {u, v, cost >= 1}; -1 if impossible2int INF = Integer.MAX_VALUE / 2;3int[][] f = new int[budget + 1][n]; // f[b][v]: fewest edges to reach v spending at most b4for (int[] row : f) Arrays.fill(row, INF);5f[0][s] = 0;6for (int b = 0; b <= budget; b++) {7if (b > 0) f[b] = f[b - 1].clone();8for (int[] e : edges)9if (e[2] <= b && f[b - e[2]][e[0]] < INF) f[b][e[1]] = Math.min(f[b][e[1]], f[b - e[2]][e[0]] + 1);10}11return f[budget][t] >= INF ? -1 : f[budget][t];12}
Multi-state graph DP: the state is (vertex, extra counter)
Interactive · edit the inputlayer 0 · 5 vertices · 8 edges
1
0
0
0
0
j = 0
W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length.
1static long[] walkCounts(int n, int[][] edges, int s, int t, int k) { // walks s -> t with exactly j edges, j = 0..k2long[] cur = new long[n];3cur[s] = 1;4long[] out = new long[k + 1];5out[0] = cur[t];6for (int j = 1; j <= k; j++) {7long[] next = new long[n];8for (int[] e : edges) next[e[1]] += cur[e[0]]; // every walk with j - 1 edges extends along every arrow9cur = next;10out[j] = cur[t];11}12return out;13}14static long cheapestAtMost(int n, int[][] edges, int s, int t, int k) { // Long.MAX_VALUE when t needs more than k edges15long INF = Long.MAX_VALUE / 2;16long[] dist = new long[n];17Arrays.fill(dist, INF);18dist[s] = 0;19for (int j = 1; j <= k; j++) {20long[] next = dist.clone(); // keeping the old value means "at most j edges"21for (int[] e : edges) if (dist[e[0]] < INF && dist[e[0]] + e[2] < next[e[1]]) next[e[1]] = dist[e[0]] + e[2];22dist = next;23}24return dist[t] >= INF ? Long.MAX_VALUE : dist[t];25}26static int fewestEdgesWithinBudget(int n, int[][] edges, int s, int t, int budget) { // weights >= 1; -1 if impossible27int INF = Integer.MAX_VALUE / 2;28int[][] f = new int[budget + 1][n]; // f[b][v]: fewest edges to reach v spending at most b29for (int[] row : f) Arrays.fill(row, INF);30f[0][s] = 0;31for (int b = 0; b <= budget; b++) {32if (b > 0) f[b] = f[b - 1].clone();33for (int[] e : edges)34if (e[2] <= b && f[b - e[2]][e[0]] < INF) f[b][e[1]] = Math.min(f[b][e[1]], f[b - e[2]][e[0]] + 1);35}36return f[budget][t] >= INF ? -1 : f[budget][t];37}38static long walksWithFewReds(int n, int[][] edges, int s, int t, int length, int maxReds) { // odd vertices are red; every visit counts39long[][] cur = new long[n][maxReds + 1];40int r0 = s % 2;41if (r0 <= maxReds) cur[s][r0] = 1;42for (int j = 1; j <= length; j++) {43long[][] next = new long[n][maxReds + 1];44for (int[] e : edges) {45int red = e[1] % 2;46for (int r = 0; r + red <= maxReds; r++) next[e[1]][r + red] += cur[e[0]][r];47}48cur = next;49}50long total = 0;51for (long x : cur[t]) total += x;52return total;53}
Live state
source
0
target
4
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | source=0 target=4 | W[j][v] counts walks from 0 to v with exactly j edges. With j = 0 only the source has a walk (the empty one). Extending a walk by one arrow gives W[j][v] = sum of W[j-1][u] over arrows u → v. Cycles are fine, because j limits the length. |
| 2 | L8 | j=1 W[1][4]=0 | Layer 1: 0 walks of exactly 1 edge from 0 to 4. |
| 3 | L8 | j=2 W[2][4]=1 | Layer 2: 1 walk of exactly 2 edges from 0 to 4. |
| 4 | L8 | j=3 W[3][4]=3 | Layer 3: 3 walks of exactly 3 edges from 0 to 4. |
| 5 | L12 | return=[0, 0, 1, 3] | Walks from 0 to 4 by length 0..3: [0, 0, 1, 3]. Each layer needs one pass over the edges: O(k · m) in total. |
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- Cheapest Flights Within K Stops
- Minimum Cost to Reach Destination in Time
- Number of Ways to Arrive at Destination
Advanced
- Shortest Path in a Grid with Obstacles Elimination
Key takeaways
- Add what matters to the state: (vertex, counter) where the counter is stops, edges, colours or money spent.
- The counter makes layers; layers form a DAG even when the graph has cycles.
- Cheapest with at most k edges: relax from the previous layer only, k + 1 rounds.
- Costs of at least 1 make the budget a forward-only counter; zero costs need Dijkstra on the states.