Reconstructing solutions
Recovering the actual solution from a dynamic programming table: parent pointers, choice tables, traceback, counting versus optimising and handling ties between several optimal solutions.
Learning goals
- Store and follow parent pointers to rebuild a solution
- Record the choice made at each state in a choice table
- Trace back through a value table without extra storage
- Tell counting problems from optimising ones and write both
- Enumerate or choose among several optimal solutions
Prerequisites
The table gives you the best value, but many problems ask for the solution itself: which items, which steps, which coins. A little extra bookkeeping recovers it.
Parent pointers
A **parent pointer** records, for every state, which previous state gave its best value. After the table is filled, start from the final state and follow the pointers back to the start; the visited states form the solution, in reverse order.
The minimum-cost-climbing animation shows it on costs 10, 15, 20, 5, 30, 8: the predecessors give the path of steps 1, 3 and 5, with total cost 28. The same bookkeeping appears in shortest paths in graphs.
1// returns the steps stood on, in order, for the cheapest climb; position n is the top2static List<Integer> cheapestSteps(int[] cost) {3int n = cost.length;4int[] dp = new int[n + 1], parent = new int[n + 1];5for (int i = 2; i <= n; i++) {6int fromOne = dp[i - 1] + cost[i - 1], fromTwo = dp[i - 2] + cost[i - 2];7if (fromOne <= fromTwo) { dp[i] = fromOne; parent[i] = i - 1; } // remember where the best value came from8else { dp[i] = fromTwo; parent[i] = i - 2; }9}10LinkedList<Integer> steps = new LinkedList<>();11for (int at = parent[n]; ; at = parent[at]) { // follow the pointers back from the top12steps.addFirst(at);13if (at <= 1) break;14}15return steps;16}
Min cost climbing stairs, with traceback
Interactive · edit the inputcost[i]
dp[i] = cheapest way to stand on i
You can start on step 0 or step 1, so dp[0] = cost[0] and dp[1] = cost[1]. Every later step is reached from one of the two steps below it.
1static int minCost(int[] cost) { // you may start on step 0 or 1 and pay cost[i] when you leave step i2int n = cost.length; int[] dp = new int[n]; // dp[i] = cheapest total to stand on step i, including cost[i]3dp[0] = cost[0]; dp[1] = cost[1];4for (int i = 2; i < n; i++) dp[i] = cost[i] + Math.min(dp[i - 1], dp[i - 2]); // best predecessor, then pay for this step5return Math.min(dp[n - 1], dp[n - 2]); // the top is one step past the end, reached from either of the last two6}
Live state
steps
6
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | steps=6 | You can start on step 0 or step 1, so dp[0] = cost[0] and dp[1] = cost[1]. Every later step is reached from one of the two steps below it. |
| 2 | L4 | i=2 dp[i-1]=15 dp[i-2]=10 dp[i]=30 | dp[2] = cost[2] + min(15, 10) = 20 + 10 = 30. The cheaper predecessor is step 0 (remembered for the traceback). |
| 3 | L4 | i=3 dp[i-1]=30 dp[i-2]=15 dp[i]=20 | dp[3] = cost[3] + min(30, 15) = 5 + 15 = 20. The cheaper predecessor is step 1 (remembered for the traceback). |
| 4 | L4 | i=4 dp[i-1]=20 dp[i-2]=30 dp[i]=50 | dp[4] = cost[4] + min(20, 30) = 30 + 20 = 50. The cheaper predecessor is step 3 (remembered for the traceback). |
| 5 | L4 | i=5 dp[i-1]=50 dp[i-2]=20 dp[i]=28 | dp[5] = cost[5] + min(50, 20) = 8 + 20 = 28. The cheaper predecessor is step 3 (remembered for the traceback). |
| 6 | L5 | top reached from=5 cost=28 | The top (one step past the end) is cheapest from step 5: total 28. |
| 7 | L5 | return=28 path=[1, 3, 5] | Minimum cost 28. Following the remembered predecessors backwards gives the steps stood on: [1, 3, 5]. |
Choice tables
A **choice table** stores the decision taken in each cell (take or skip, which coin, which split) in a second table the same shape as the value table. Reading the choices from the final cell backwards reconstructs the solution. It costs extra memory but makes the traceback trivial and is the natural tool when the decision is not recoverable from the values alone.
In the knapsack animation (the “2D table” preset), a cell that differs from the cell above it means the item was taken, so the choices are read directly off the table: items #1 and #2 are taken for a value of 35.
1// returns the 1-based indices of the chosen items2static List<Integer> chosenItems(int[][] items, int capacity) {3int n = items.length;4int[][] dp = new int[n + 1][capacity + 1];5boolean[][] took = new boolean[n + 1][capacity + 1]; // the choice made in each cell6for (int i = 1; i <= n; i++) {7for (int w = 0; w <= capacity; w++) {8dp[i][w] = dp[i - 1][w];9if (items[i - 1][0] <= w && dp[i - 1][w - items[i - 1][0]] + items[i - 1][1] > dp[i][w]) {10dp[i][w] = dp[i - 1][w - items[i - 1][0]] + items[i - 1][1];11took[i][w] = true;12}13}14}15LinkedList<Integer> chosen = new LinkedList<>();16for (int i = n, w = capacity; i >= 1; i--) {17if (took[i][w]) { chosen.addFirst(i); w -= items[i - 1][0]; }18}19return chosen;20}
0/1 knapsack: 2D table, and why 1D must run backwards
Interactive · edit the inputgrid · 5 × 6
dp[i][c] is the best value from the first i items with capacity c. Row 0 is all zeros. Each cell either skips item i (copy the cell above) or takes it (value + the cell above-left by the item's weight).
1static int knapsack2D(int[] w, int[] v, int cap) {2int n = w.length; int[][] dp = new int[n + 1][cap + 1]; // dp[i][c]: best value from the first i items with capacity c3for (int i = 1; i <= n; i++)4for (int c = 0; c <= cap; c++) {5dp[i][c] = dp[i - 1][c]; // skip item i6if (c >= w[i - 1]) dp[i][c] = Math.max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]); // or take it7}8return dp[n][cap];9}10static int knapsack1D(int[] w, int[] v, int cap, boolean backwards) {11int[] dp = new int[cap + 1];12for (int i = 0; i < w.length; i++) {13if (backwards) for (int c = cap; c >= w[i]; c--) dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]); // right to left: dp[c - w] still excludes item i14else for (int c = w[i]; c <= cap; c++) dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]); // left to right: item i may be taken again (a bug for 0/1)15}16return dp[cap];17}
Live state
items
3
capacity
4
Execution trace (dry run table)Show 17 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | items=3 capacity=4 | dp[i][c] is the best value from the first i items with capacity c. Row 0 is all zeros. Each cell either skips item i (copy the cell above) or takes it (value + the cell above-left by the item's weight). |
| 2 | L6 | item=1 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 1 (weight 1) does not fit in capacity 0: copy the cell above (0). |
| 3 | L6 | item=1 c=1 skip=0 take=15 dp[i][c]=15 | Capacity 1: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 4 | L6 | item=1 c=2 skip=0 take=15 dp[i][c]=15 | Capacity 2: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 5 | L6 | item=1 c=3 skip=0 take=15 dp[i][c]=15 | Capacity 3: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 6 | L6 | item=1 c=4 skip=0 take=15 dp[i][c]=15 | Capacity 4: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 7 | L6 | item=2 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 2 (weight 3) does not fit in capacity 0: copy the cell above (0). |
| 8 | L6 | item=2 c=1 skip=15 take=does not fit dp[i][c]=15 | Item 2 (weight 3) does not fit in capacity 1: copy the cell above (15). |
| 9 | L6 | item=2 c=2 skip=15 take=does not fit dp[i][c]=15 | Item 2 (weight 3) does not fit in capacity 2: copy the cell above (15). |
| 10 | L6 | item=2 c=3 skip=15 take=20 dp[i][c]=20 | Capacity 3: skip item 2 → 15; take it → 0 + 20 = 20. Keep 20. |
| 11 | L6 | item=2 c=4 skip=15 take=35 dp[i][c]=35 | Capacity 4: skip item 2 → 15; take it → 15 + 20 = 35. Keep 35. |
| 12 | L6 | item=3 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 3 (weight 4) does not fit in capacity 0: copy the cell above (0). |
| 13 | L6 | item=3 c=1 skip=15 take=does not fit dp[i][c]=15 | Item 3 (weight 4) does not fit in capacity 1: copy the cell above (15). |
| 14 | L6 | item=3 c=2 skip=15 take=does not fit dp[i][c]=15 | Item 3 (weight 4) does not fit in capacity 2: copy the cell above (15). |
| 15 | L6 | item=3 c=3 skip=20 take=does not fit dp[i][c]=20 | Item 3 (weight 4) does not fit in capacity 3: copy the cell above (20). |
| 16 | L6 | item=3 c=4 skip=35 take=30 dp[i][c]=35 | Capacity 4: skip item 3 → 35; take it → 0 + 30 = 30. Keep 35. |
| 17 | L8 | return=35 items=[#1, #2] | Best value 35. Traceback from the bottom-right cell: whenever a cell differs from the one above it, that item was taken (items [#1, #2]), and we move left by its weight. |
Traceback
A **traceback** recovers the solution from the value table alone, by re-asking at each state which option produced the stored value. No extra table is needed; you pay a second, linear pass. For house robber, start at house n: if best[i] == best[i - 1] the house was skipped, otherwise it was robbed and you jump to house i - 2.
On the “Classic” preset (2, 7, 9, 3, 1) the traceback goes house 5 (robbed, 12 differs from 11), house 3 (robbed), house 1 (robbed), giving houses 1, 3 and 5.
1// best[i] from the earlier lesson: best loot from the first i houses; returns the robbed houses (1-based)2static List<Integer> robbedHouses(int[] value) {3int n = value.length;4int[] best = new int[n + 1];5for (int i = 1; i <= n; i++) best[i] = Math.max(best[i - 1], (i >= 2 ? best[i - 2] : 0) + value[i - 1]);6LinkedList<Integer> houses = new LinkedList<>();7int i = n;8while (i >= 1) {9if (best[i] == best[i - 1]) i--; // skipped: the value did not change10else { houses.addFirst(i); i -= 2; } // robbed: jump over the neighbour11}12return houses;13}
House robber: take it or skip it
Interactive · edit the inputhouse money
dp[i] = best from the first i houses
For the first i houses, the best loot either skips house i (best of the first i−1) or robs it (its money plus the best of the first i−2, because the neighbour is off limits).
1static int rob(int[] a) { // no two adjacent houses2int n = a.length; int[] dp = new int[n + 1]; // dp[i] = best loot from the first i houses3dp[1] = a[0];4for (int i = 2; i <= n; i++)5dp[i] = Math.max(dp[i - 1], dp[i - 2] + a[i - 1]); // skip house i, or rob it and use the best before the neighbour6return dp[n];7}
Live state
houses
5
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | houses=5 | For the first i houses, the best loot either skips house i (best of the first i−1) or robs it (its money plus the best of the first i−2, because the neighbour is off limits). |
| 2 | L5 | i=2 skip=2 rob=7 dp[i]=7 | House 2: skipping keeps 2; robbing gives dp[0] + 7 = 7. Best 7 (rob it). |
| 3 | L5 | i=3 skip=7 rob=11 dp[i]=11 | House 3: skipping keeps 7; robbing gives dp[1] + 9 = 11. Best 11 (rob it). |
| 4 | L5 | i=4 skip=11 rob=10 dp[i]=11 | House 4: skipping keeps 11; robbing gives dp[2] + 3 = 10. Best 11 (skip it). |
| 5 | L5 | i=5 skip=11 rob=12 dp[i]=12 | House 5: skipping keeps 11; robbing gives dp[3] + 1 = 12. Best 12 (rob it). |
| 6 | L6 | return=12 houses=[1, 3, 5] | Maximum loot 12. Tracing the take/skip decisions backwards from house 5 gives the houses [1, 3, 5]. |
Counting vs optimizing
The same table shape answers two kinds of questions. An **optimising** DP combines options with min or max; a **counting** DP combines them with +. Coin change shows both: the fewest coins use min over the last coin, and the number of ways to make an amount uses +.
Counting has a further subtlety: **order**. If the loops run coins outside and amounts inside, each combination is counted once (the coins used are fixed in the order of the outer loop). If amounts run outside and coins inside, every ordered sequence is counted. For coins 1, 2, 5 and amount 11 there are 11 combinations and 218 ordered sequences. The animation's “Count combinations” preset reports the 11.
1// number of combinations (order does not matter): coins outer, amounts inner2static long countCombinations(int[] coins, int amount) {3long[] ways = new long[amount + 1];4ways[0] = 1;5for (int c : coins) for (int a = c; a <= amount; a++) ways[a] += ways[a - c];6return ways[amount];7}89// number of ordered sequences: amounts outer, coins inner10static long countSequences(int[] coins, int amount) {11long[] ways = new long[amount + 1];12ways[0] = 1;13for (int a = 1; a <= amount; a++) for (int c : coins) if (c <= a) ways[a] += ways[a - c];14return ways[amount];15}
Coin change: fewest coins, or number of ways
Interactive · edit the inputdp[a] = fewest coins
last coin used
dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c.
1static int minCoins(int[] coins, int amount) {2int INF = amount + 1;3int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; // dp[a] = fewest coins that make a4for (int a = 1; a <= amount; a++)5for (int c : coins)6if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1; // one coin c, then solve a - c7return dp[amount] >= INF ? -1 : dp[amount];8}9static long ways(int[] coins, int amount) {10long[] dp = new long[amount + 1]; dp[0] = 1; // one way to make 0: take nothing11for (int c : coins) // coins in the outer loop: each combination is counted once12for (int a = c; a <= amount; a++) dp[a] += dp[a - c];13return dp[amount];14}
Live state
amount
6
coins
[1, 3, 4]
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | amount=6 coins=[1, 3, 4] | dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c. |
| 2 | L6 | a=1 coin=1 candidate=1 dp[a]=1 | Coin 1: dp[0] + 1 = 1 beats the old value: dp[1] = 1. |
| 3 | L6 | a=2 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[1] + 1 = 2 beats the old value: dp[2] = 2. |
| 4 | L6 | a=3 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[2] + 1 = 3 beats the old value: dp[3] = 3. |
| 5 | L6 | a=3 coin=3 candidate=1 dp[a]=1 | Coin 3: dp[0] + 1 = 1 beats the old value: dp[3] = 1. |
| 6 | L6 | a=4 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[3] + 1 = 2 beats the old value: dp[4] = 2. |
| 7 | L6 | a=4 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[1] + 1 = 2 does not improve dp[4] = 2. |
| 8 | L6 | a=4 coin=4 candidate=1 dp[a]=1 | Coin 4: dp[0] + 1 = 1 beats the old value: dp[4] = 1. |
| 9 | L6 | a=5 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[4] + 1 = 2 beats the old value: dp[5] = 2. |
| 10 | L6 | a=5 coin=3 candidate=3 dp[a]=2 | Coin 3: dp[2] + 1 = 3 does not improve dp[5] = 2. |
| 11 | L6 | a=5 coin=4 candidate=2 dp[a]=2 | Coin 4: dp[1] + 1 = 2 does not improve dp[5] = 2. |
| 12 | L6 | a=6 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[5] + 1 = 3 beats the old value: dp[6] = 3. |
| 13 | L6 | a=6 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[3] + 1 = 2 beats the old value: dp[6] = 2. |
| 14 | L6 | a=6 coin=4 candidate=3 dp[a]=2 | Coin 4: dp[2] + 1 = 3 does not improve dp[6] = 2. |
| 15 | L7 | return=2 coins=[3, 3] | Fewest coins: 2. Following the last coin used from 6 down to 0 gives the coins [3, 3]. Other optimal combinations may exist; the traceback shows the one the table remembers. |
Multiple optimal solutions
The best value is unique, but the solution achieving it often is not. Coins 1, 2, 3 and amount 4 can be made with two coins as 1 + 3 or as 2 + 2; the “Several optimal answers” preset shows the traceback choosing 1 + 3 and notes that others may exist. Which one you get depends on tie-breaking: < versus <= in the comparison, or the order the options are tried.
If any optimal solution is acceptable, take whichever the traceback finds, but make the tie-break deterministic. If the problem asks for a specific one (lexicographically smallest, fewest items among best-value solutions) fold the tie-break into the transition. To list all optimal solutions, search the table: from each state, follow every option whose value is consistent with the optimum, as in the code below, which lists every optimal multiset of coins.
1// all coin multisets with the fewest coins, each listed in non-decreasing order; coins must be sorted ascending2static List<List<Integer>> optimalCombinations(int[] coins, int amount) {3int[] dp = new int[amount + 1];4Arrays.fill(dp, Integer.MAX_VALUE);5dp[0] = 0;6for (int a = 1; a <= amount; a++)7for (int c : coins) if (c <= a && dp[a - c] != Integer.MAX_VALUE) dp[a] = Math.min(dp[a], dp[a - c] + 1);8List<List<Integer>> out = new ArrayList<>();9if (dp[amount] != Integer.MAX_VALUE) collect(coins, dp, amount, coins.length - 1, new ArrayDeque<>(), out);10return out;11}1213// walk the table from 'remaining' down to 0, choosing coins in non-increasing order to avoid duplicates14static void collect(int[] coins, int[] dp, int remaining, int maxIndex, Deque<Integer> path, List<List<Integer>> out) {15if (remaining == 0) { out.add(new ArrayList<>(path)); return; }16for (int i = maxIndex; i >= 0; i--) {17int c = coins[i];18if (c <= remaining && dp[remaining - c] == dp[remaining] - 1) { // this coin lies on some optimal solution19path.addFirst(c);20collect(coins, dp, remaining - c, i, path, out);21path.removeFirst();22}23}24}
Coin change: fewest coins, or number of ways
Interactive · edit the inputdp[a] = fewest coins
last coin used
dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c.
1static int minCoins(int[] coins, int amount) {2int INF = amount + 1;3int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; // dp[a] = fewest coins that make a4for (int a = 1; a <= amount; a++)5for (int c : coins)6if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1; // one coin c, then solve a - c7return dp[amount] >= INF ? -1 : dp[amount];8}9static long ways(int[] coins, int amount) {10long[] dp = new long[amount + 1]; dp[0] = 1; // one way to make 0: take nothing11for (int c : coins) // coins in the outer loop: each combination is counted once12for (int a = c; a <= amount; a++) dp[a] += dp[a - c];13return dp[amount];14}
Live state
amount
6
coins
[1, 3, 4]
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | amount=6 coins=[1, 3, 4] | dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c. |
| 2 | L6 | a=1 coin=1 candidate=1 dp[a]=1 | Coin 1: dp[0] + 1 = 1 beats the old value: dp[1] = 1. |
| 3 | L6 | a=2 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[1] + 1 = 2 beats the old value: dp[2] = 2. |
| 4 | L6 | a=3 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[2] + 1 = 3 beats the old value: dp[3] = 3. |
| 5 | L6 | a=3 coin=3 candidate=1 dp[a]=1 | Coin 3: dp[0] + 1 = 1 beats the old value: dp[3] = 1. |
| 6 | L6 | a=4 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[3] + 1 = 2 beats the old value: dp[4] = 2. |
| 7 | L6 | a=4 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[1] + 1 = 2 does not improve dp[4] = 2. |
| 8 | L6 | a=4 coin=4 candidate=1 dp[a]=1 | Coin 4: dp[0] + 1 = 1 beats the old value: dp[4] = 1. |
| 9 | L6 | a=5 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[4] + 1 = 2 beats the old value: dp[5] = 2. |
| 10 | L6 | a=5 coin=3 candidate=3 dp[a]=2 | Coin 3: dp[2] + 1 = 3 does not improve dp[5] = 2. |
| 11 | L6 | a=5 coin=4 candidate=2 dp[a]=2 | Coin 4: dp[1] + 1 = 2 does not improve dp[5] = 2. |
| 12 | L6 | a=6 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[5] + 1 = 3 beats the old value: dp[6] = 3. |
| 13 | L6 | a=6 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[3] + 1 = 2 beats the old value: dp[6] = 2. |
| 14 | L6 | a=6 coin=4 candidate=3 dp[a]=2 | Coin 4: dp[2] + 1 = 3 does not improve dp[6] = 2. |
| 15 | L7 | return=2 coins=[3, 3] | Fewest coins: 2. Following the last coin used from 6 down to 0 gives the coins [3, 3]. Other optimal combinations may exist; the traceback shows the one the table remembers. |
Practice ladder
Now try it yourself
Easy
- Min Cost Climbing Stairs (path)
- Climbing Stairs (count)
Medium
- Coin Change II
- Longest Increasing Subsequence (print one)
- Target Sum
Advanced
- Longest Palindromic Subsequence (print)
- Shortest Common Supersequence
Key takeaways
- Parent pointers record where each state's best value came from; follow them back from the goal.
- A choice table stores the decision per cell; a traceback re-derives it from the values without storage.
- Counting uses + and optimising uses min or max over the same state structure.
- Loop order decides whether combinations or ordered sequences are counted.
- Ties give several optimal solutions: fix a tie-break, or enumerate them from the table.