Data Structures & Algorithms / Optimization proofs and maintained aggregates / Lesson 13
Dynamic programming: state, order, and reconstruction
Define reusable subproblems and a valid dependency order before compressing storage or reconstructing a solution.
On this page
Lesson goals
- A table entry needs a sentence of meaning
- Minimize over every valid last choice
- Compressed storage still represents conceptual layers
A table entry needs a sentence of meaning
Dynamic programming reuses solutions to overlapping subproblems. Start by defining exactly what each state means, its accepted parameters, and which smaller states it depends on. Then define base cases and an evaluation order that respects those dependencies. A table full of numbers is not a model by itself. For minimum coin count, state[a] can mean the minimum number of permitted positive coins whose values sum exactly to amount a, with unlimited reuse. This differs from counting combinations, choosing each coin once, or accepting a sum no greater than a. Each variation changes the recurrence.
Minimize over every valid last choice
The first program considers the final coin of an exact amount. Removing a positive coin leaves a strictly smaller amount, so increasing amount order is valid. Unreachable states remain infinity, while amount zero needs zero coins. A predecessor coin records a choice only when it improves the count. Following these choices reconstructs one optimum, and positivity guarantees the remaining amount decreases to zero. The code rejects zero or negative denominations because they would invalidate this dependency argument. It returns None for an unreachable target instead of confusing absence with an accepted zero-coin solution.
from math import inf
def minimum_coins(coins, amount):
if type(amount) is not int or amount < 0:
raise ValueError("Amount must be a nonnegative integer")
if any(type(coin) is not int or coin <= 0 for coin in coins):
raise ValueError("Coins must be positive integers")
best = [inf] * (amount + 1)
parent = [None] * (amount + 1)
best[0] = 0
for current in range(1, amount + 1):
for coin in coins:
if coin <= current and best[current - coin] + 1 < best[current]:
best[current] = best[current - coin] + 1
parent[current] = coin
if best[amount] == inf:
return None
chosen, remaining = [], amount
while remaining:
coin = parent[remaining]
chosen.append(coin)
remaining -= coin
return best[amount], chosen
print(minimum_coins([1, 3, 4], 6))
print(minimum_coins([4, 6], 5))
print(minimum_coins([1, 3], 0))
(2, [3, 3])
None
(0, [])
Exact6 uses two3 coins. Amount5 cannot be formed from4 and6, while zero has the empty solution.
Compressed storage still represents conceptual layers
In zero-one knapsack, each item can be used at most once. A conceptual state includes both the processed-item prefix and capacity. The second program stores only one capacity row, updating capacities downward so a transition reads the previous item layer. Upward iteration would read a value already improved with the same item and accidentally permit reuse. The state here means best value with weight at most capacity, so zero initialization permits the empty selection. An exact-weight formulation needs a different unreachable-state initialization. Compression changes storage, not the underlying dependency contract.
def zero_one_knapsack(items, capacity):
if type(capacity) is not int or capacity < 0:
raise ValueError("Capacity must be a nonnegative integer")
best = [0] * (capacity + 1)
for weight, value in items:
if type(weight) is not int or weight <= 0:
raise ValueError("Weights must be positive integers")
for current in range(capacity, weight - 1, -1):
best[current] = max(best[current], best[current - weight] + value)
return best[capacity]
print(zero_one_knapsack([(2, 3), (3, 4)], 4))
print(zero_one_knapsack([(2, 3)], 0))
4
0
The weight2 item cannot be used twice. Descending iteration preserves the previous-item meaning of the source cell.
Numeric bounds can make complexity pseudopolynomial
For c denominations and target A, the coin table uses O(cA) transitions and O(A) storage, excluding reconstruction output. Knapsack uses O(nC) transitions for n items and capacity C. These bounds depend on numeric magnitude, while writing A or C in binary takes only logarithmically many bits. Therefore the algorithms are pseudopolynomial in general input encoding, not polynomial merely because they use nested loops over integers. Large values can make a table impractical despite a small number of items. State arithmetic assumptions and accepted resource limits without prescribing a universal maximum.
Reconstruction needs consistency with the optimum
A correct objective value does not guarantee a correct reconstructed choice list. Every stored predecessor must correspond to the transition that established that state, and its chain must terminate under the dependency measure. Memory compression can overwrite information needed for reconstruction, especially in item-based problems. Preserve separate predecessor layers, recompute decisions deliberately, or use another proven recovery strategy. Also define tie behavior: several optima may exist, and returning one valid optimum is different from counting all or selecting a lexicographically preferred optimum. Do not infer a unique solution from a unique numeric minimum.
Compare tiny states with an independent enumeration
For small coin targets, a BFS over reachable amounts provides an independent minimum-edge reference. Verify the reconstructed coins belong to the accepted set, sum to the target, and have the reported count. For tiny knapsack fixtures, enumerate subsets and compare the best feasible value. Include unreachable exact amounts, zero target, duplicate denominations, an item heavier than capacity, and the reuse counterexample. These tests are deliberately bounded local computations. They complement the recurrence and layer invariant, and do not claim that finite enumeration proves every input or that large tables were benchmarked.
Common errors and fixes
A zero-one item is reused within one update
- Why it happens
- Compressed capacities were traversed upward.
- How to fix it
- Traverse downward to read the previous item layer.
- Prevent it next time
- Test one weight2,value3 item at capacity4.
An unreachable amount looks like an empty solution
- Why it happens
- All states were initialized as accepted zero cost.
- How to fix it
- Keep unreachable distinct and initialize only valid base states.
- Prevent it next time
- Test zero separately from a positive unreachable amount.
Reconstruction loops or returns inconsistent choices
- Why it happens
- Predecessors do not follow a decreasing valid transition.
- How to fix it
- Store choices alongside proven improvements and validate the chain.
- Prevent it next time
- Check membership, sum, count, and termination.
Check your understanding
1. Why does largest-first fail for coins1,3,4 and amount6?
Show worked solution
It chooses4 then1 then1, using three coins, while3+3 uses two. A local large denomination does not guarantee a globally minimal count. The dynamic program considers every permitted final coin and compares complete smaller solutions.
2. Why does the knapsack result4 differ from an unbounded result6?
Show worked solution
With capacity4, the weight2,value3 item could produce6 only if selected twice. The zero-one contract permits it once, and the best permitted single item is weight3,value4. Descending updates encode that once-only restriction.
Key takeaways
- A table entry needs a sentence of meaning
- Compare tiny states with an independent enumeration