Author: Hang Li (@flgkd)
Note: Titles marked with ⭐ highlight key concepts and important summaries.
This note studies dynamic programming (DP), one of the most important methods for solving multi-stage decision problems.
This note is primarily based on Sun and Li's Integer Programming [1].
Dynamic programming is particularly useful when a problem can be divided into stages and the optimal decision at one stage can be expressed through optimal solutions of smaller subproblems. In integer programming, it can be used to solve several important special structures, including the 0-1 knapsack problem and the integer knapsack problem [1].
The main examples in this note are:
- shortest-path problems;
- 0-1 knapsack;
- integer / unbounded knapsack;
- uncapacitated lot-sizing.
One point is especially important after the previous note on computational complexity:
A dynamic-programming algorithm does not automatically imply a polynomial-time algorithm.
For knapsack, the familiar
For a review of this distinction, see:
The basic idea of dynamic programming is also to decompose a problem into smaller subproblems.
The important difference from ordinary divide-and-conquer is that the subproblems in a dynamic-programming formulation are often not independent.
A naive recursive method may repeatedly solve the same subproblem many times.
Dynamic programming avoids this repeated computation by storing the value of each state after it has been solved.
There are two standard implementations:
top-down recursion + memoization
bottom-up recursion + tabulation
In this note, the main presentation follows the second style: define a state-value function, derive a recurrence, and fill a table stage by stage.
A useful practical summary of the usual conditions for applying dynamic programming is:
- the principle of optimality / optimal substructure;
- no aftereffect;
- overlapping subproblems.
This is a very useful practical checklist, but one qualification is needed.
Optimal substructure and a sufficient state description are the fundamental structural requirements. Overlapping subproblems are what often make memoization or tabulation computationally useful, but they are not a formal requirement for every dynamic-programming formulation.
A problem has optimal substructure if an optimal solution contains optimal solutions to the relevant subproblems.
For example, if a shortest path from
Once the current state is known, future decisions should not need the full history of how that state was reached.
In other words:
The state should contain all information from the past that is relevant to the future.
This is closely related to the Markov idea in sequential decision problems.
Different decision sequences may lead to the same state.
If the value of that state is computed once and stored, many repeated calculations can be avoided.
This is why dynamic programming often trades memory for speed.
However, it is too strong to say that DP always converts an exponential algorithm into a polynomial one. The number of distinct states may itself be exponential or pseudo-polynomial.
For a multi-stage minimization problem, let
-
$k$ be the stage; -
$s$ be the current state; -
$a$ be a feasible decision; -
$g_k(s,a)$ be the immediate cost; -
$T_k(s,a)$ be the next state.
A generic Bellman recursion is
For a maximization problem, replace
The terminal condition may be written as
This simple structure is the core idea behind all of the examples below.
Richard Bellman's work in the 1950s developed this principle into the systematic framework now known as dynamic programming [2].
Let
be a directed graph, where
For each arc
let
For the DAG recursion discussed below, the arc lengths do not need to be positive; the absence of directed cycles is enough.
Given a source node
The most direct idea is enumeration:
enumerate all s-t paths
→ calculate every path length
→ choose the shortest
But the number of paths can grow exponentially with the size of the graph.
An s-t shortest path and one intermediate node p.
Suppose a shortest path from
Then:
The subpath from
$s$ to$p$ is a shortest path from$s$ to$p$ , and the subpath from$p$ to$t$ is a shortest path from$p$ to$t$ .
The proof is by contradiction.
If the subpath from
This is the shortest-path version of the principle of optimality.
Let
Let
be the set of predecessors of
Then
and, whenever the predecessor distances are already known,
This equation says:
If I already know the shortest distances to every node that can directly enter
$v$ , then I can compute the shortest distance to$v$ .
This is exactly a dynamic-programming recurrence.
For a general directed graph, the equation above is not by itself an algorithm, because dependencies may form cycles.
For a directed acyclic graph (DAG), however, the nodes can be arranged in topological order.
Then every predecessor of a node appears earlier in the order, so the recurrence can be evaluated once from left to right.
A DAG shortest-path algorithm therefore runs in
time when the graph is stored using adjacency lists [3].
This bound follows directly from processing the nodes once in topological order.
The
Let
be the shortest distance from
Initialize
and
Then
If there is no reachable negative cycle, a shortest simple path uses at most
arcs, so computing the recurrence for
gives the Bellman-Ford dynamic program with complexity
So there are two different facts to remember:
DAG + topological order
→ O(|V| + |A|)
general directed graph + Bellman-Ford recurrence
→ O(|V||A|)
For nonnegative arc lengths, Dijkstra's algorithm is another standard alternative.
Consider the graph below.
The task is to determine the shortest distance from node
Shortest Path from A to J.
The stages are
Stage 0: A
Stage 1: B, C, D
Stage 2: E, F, G
Stage 3: H, I
Stage 4: J
From
For Stage 2:
For Stage 3:
Finally,
Therefore the shortest-path length is
There are two shortest paths:
and
Both have length
This example contains 18 complete paths from
An enumeration algorithm evaluates all
Every complete path contains four arcs, so evaluating one path length requires three additions. Under the simple convention of counting only additions and comparisons, exhaustive enumeration requires
elementary operations.
Dynamic programming instead computes the best value of each intermediate state only once.
Using the same counting convention, the stage-by-stage calculations require:
- Stage 2:
$9$ additions and$6$ comparisons; - Stage 3:
$6$ additions and$4$ comparisons; - Stage 4:
$2$ additions and$1$ comparison.
Thus the recurrence requires
additions and comparisons for this example, excluding simple initialization and assignment operations.
The exact count depends on the implementation and the operation-counting convention, so the main point is not the particular number
The important difference is structural:
enumeration
→ work grows with the number of complete paths
dynamic programming on this DAG
→ work grows with the number of states and arcs
For large layered networks, the difference can be enormous.
from math import inf
def dag_shortest_path(topological_order, edges, source):
adjacency = {u: [] for u in topological_order}
for u, v, weight in edges:
adjacency[u].append((v, weight))
distance = {u: inf for u in topological_order}
parent = {u: None for u in topological_order}
distance[source] = 0
for u in topological_order:
if distance[u] == inf:
continue
for v, weight in adjacency[u]:
candidate = distance[u] + weight
if candidate < distance[v]:
distance[v] = candidate
parent[v] = u
return distance, parentFor the graph above, one topological order is
order = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]The algorithm scans each arc once after the topological ordering is known.
A stage is one part of a sequential decision process. The stage index is commonly denoted by
A state describes the information needed at the beginning of that stage. A state variable may be denoted by
In a layered shortest-path problem, nodes can be grouped into stages according to their position in the path. In the graph above, every node in the same stage is reached after the same number of arcs from the source.
The stages are therefore:
- Stage 1 states:
$B,C,D$ ; - Stage 2 states:
$E,F,G$ ; - Stage 3 states:
$H,I$ ; - Stage 4 state:
$J$ .
The state is what summarizes the past.
Once we know the shortest distance to the current node, we do not need to remember every path used to reach it.
This distinction between stage and state is important:
stage
→ where we are in the decision process
state
→ the information needed to continue optimally from that stage
The knapsack problem is one of the standard examples for understanding both dynamic programming and pseudo-polynomial complexity [4].
Consider the 0-1 knapsack problem
subject to
Here:
-
$c_j$ is the value of item$j$ ; -
$a_j$ is the weight of item$j$ ; -
$B$ is the knapsack capacity; -
$x_j=1$ means that item$j$ is selected.
For the capacity-based dynamic program, assume
and
The profits
Let the stage
We are allowed to use only items
$1,\ldots,k$ .
Let the state
Define
The original problem value is
A convenient initialization is
For
and
For item
Then
and the best value is
If
then selecting item
The remaining best value is
so the total value is
Therefore,
Otherwise,
This is the standard 0-1 knapsack recurrence.
The reason the second term uses
rather than
is crucial:
Item
$k$ can be used at most once.
After the table is filled, start from
If
we may choose
Otherwise,
and update
Then continue with
When there is a tie, more than one optimal solution may exist. The chosen solution depends on the tie-breaking rule.
There are
states.
Each state requires constant work.
Therefore the time complexity is
The full two-dimensional table uses
memory.
If only the optimal value is needed, memory can be reduced to
by updating capacities in descending order.
This is one of the most important points in the note.
At first sight,
looks polynomial.
But standard complexity is measured in terms of the number of bits required to encode the input.
The binary encoding length of
Let
Then
Therefore the running time may behave like
which is exponential in the number of bits used to represent the capacity.
A more complete estimate of the binary input length for integer data is
So the
This does not contradict the NP-hardness of 0-1 knapsack.
Suppose we sort
If each integer uses
An
Now consider trial division for testing whether one integer
def is_prime_trial(N):
if N < 2:
return False
divisor = 2
while divisor * divisor <= N:
if N % divisor == 0:
return False
divisor += 1
return TrueThe loop performs on the order of
iterations.
But
bits to encode.
If
then
So this particular trial-division algorithm is not polynomial in the bit length.
This does not mean primality testing itself is hard: Agrawal, Kayal, and Saxena proved that primality testing has a deterministic polynomial-time algorithm [5].
The lesson is simply:
Never confuse the numerical magnitude of an integer with the length of its binary encoding.
This distinction is easy to miss because, for many ordinary data structures, the natural size parameter and the encoded input length grow together.
For example, if a graph is stored as an adjacency list, merely listing its vertices and arcs already requires a representation whose structural size grows with
A numerical parameter such as
If
This is the intuition behind pseudo-polynomial time.
It is also worth remembering that pseudo-polynomial does not mean useless in practice. If
def knapsack_01(weights, profits, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
take = [[False] * (capacity + 1) for _ in range(n + 1)]
for k in range(1, n + 1):
weight = weights[k - 1]
profit = profits[k - 1]
for cap in range(capacity + 1):
dp[k][cap] = dp[k - 1][cap]
if weight <= cap:
candidate = profit + dp[k - 1][cap - weight]
if candidate > dp[k][cap]:
dp[k][cap] = candidate
take[k][cap] = True
x = [0] * n
cap = capacity
for k in range(n, 0, -1):
if take[k][cap]:
x[k - 1] = 1
cap -= weights[k - 1]
return dp[n][capacity], x, dpThe task is to determine the maximum total value and recover an optimal 0-1 item-selection vector for the following knapsack problem.
Consider
subject to
The DP table is:
| Capacity |
||||||
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 3 | 3 | 3 | 3 | 3 | 3 |
| 3 | 3 | 3 | 3 | 3 | 6 | 6 |
| 4 | 3 | 3 | 5 | 5 | 6 | 6 |
| 5 | 3 | 3 | 5 | 5 | 9 | 9 |
| 6 | 3 | 3 | 5 | 5 | 9 | 9 |
| 7 | 3 | 3 | 5 | 5 | 11 | 11 |
| 8 | 3 | 5 | 5 | 7 | 11 | 11 |
| 9 | 3 | 5 | 5 | 7 | 11 | 11 |
| 10 | 3 | 5 | 7 | 7 | 11 | 11 |
| 11 | 3 | 5 | 7 | 7 | 13 | 13 |
| 12 | 3 | 5 | 7 | 7 | 13 | 13 |
| 13 | 3 | 5 | 7 | 7 | 13 | 13 |
| 14 | 3 | 5 | 7 | 9 | 13 | 13 |
| 15 | 3 | 5 | 7 | 9 | 13 | 13 |
| 16 | 3 | 5 | 7 | 9 | 13 | 15 |
| 17 | 3 | 5 | 7 | 9 | 15 | 15 |
Therefore,
The problem has more than one optimal solution.
For example,
has weight
and value
Another optimal solution is
which exactly uses capacity
This is a useful reminder that a DP value table determines the optimal objective value, but tie-breaking during backtracking determines which optimal solution is returned.
Now consider the integer, or unbounded, knapsack problem
subject to
The difference from 0-1 knapsack is that each item type may now be selected multiple times.
For the capacity-based dynamic programs below, assume
and
The positive-weight assumption is essential: if an item had zero weight and positive profit, the unbounded knapsack problem would itself be unbounded.
Define
Then the original problem value is
Suppose item type
Then
After using
Therefore,
For one state, this can require up to
Across all stages and capacities, the worst-case complexity is therefore
The direct recurrence can be improved.
For an optimal solution of
Then the value is
Remove one copy of item
The remaining problem still allows item
Therefore,
Otherwise,
Notice the difference from 0-1 knapsack:
0-1 knapsack:
c_k + F_{k-1}(lambda - a_k)
unbounded knapsack:
c_k + G_k(lambda - a_k)
The use of
The complexity is now
This is still pseudo-polynomial.
Starting from
if
choose no additional item of type
Otherwise, choose one more copy:
and update
The stage remains
def unbounded_knapsack(weights, profits, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
take = [[False] * (capacity + 1) for _ in range(n + 1)]
for k in range(1, n + 1):
weight = weights[k - 1]
profit = profits[k - 1]
for cap in range(capacity + 1):
dp[k][cap] = dp[k - 1][cap]
if weight <= cap:
candidate = profit + dp[k][cap - weight]
if candidate > dp[k][cap]:
dp[k][cap] = candidate
take[k][cap] = True
x = [0] * n
k = n
cap = capacity
while k > 0:
if take[k][cap]:
x[k - 1] += 1
cap -= weights[k - 1]
else:
k -= 1
return dp[n][capacity], x, dpThe task is to determine the maximum total value and recover an optimal integer item-selection vector for the following unbounded knapsack problem.
Consider
subject to
The DP table is:
| Capacity |
||||
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 2 | 2 |
| 3 | 7 | 7 | 7 | 7 |
| 4 | 7 | 9 | 9 | 9 |
| 5 | 7 | 9 | 9 | 9 |
| 6 | 14 | 14 | 14 | 14 |
| 7 | 14 | 16 | 16 | 16 |
| 8 | 14 | 18 | 18 | 18 |
| 9 | 21 | 21 | 21 | 21 |
| 10 | 21 | 23 | 23 | 23 |
Hence,
One optimal solution is
Indeed,
and
The same integer knapsack problem can also be written without the stage index.
Define
Set
For
The recurrence can be justified in both directions.
For any item type
take an optimal solution for capacity
Conversely, suppose an optimal solution for capacity
Together, these two inequalities establish the recurrence.
For every fixed capacity
Therefore the total complexity remains
Since
For the numerical example above,
The one-dimensional recurrence has a direct graph interpretation.
Create a DAG with nodes
For every item type
add an arc
with length
Taking one copy of item
Because all arcs move from a smaller capacity to a larger capacity, the graph is acyclic.
If unused capacity must be represented explicitly, one may also add zero-weight arcs
For every capacity state
In particular, the integer knapsack optimum is the longest-path value from node
The figure below corresponds to
subject to
The zero-weight arcs are omitted from the picture.
Knapsack Problem as a Longest Path Problem.
This graph interpretation makes the relation between dynamic programming and shortest / longest paths very concrete:
A DP state can often be viewed as a node, and a decision can often be viewed as an arc between states.
The Uncapacitated Lot-Sizing Problem (ULS) determines a minimum-cost production and inventory schedule for one product over a finite discrete planning horizon while satisfying known demand in every period.
For period
let
-
$f_t$ : fixed setup cost if production occurs in period$t$ ; -
$p_t$ : unit production cost; -
$h_t$ : unit inventory holding cost at the end of period$t$ ; -
$d_t$ : demand; -
$x_t$ : production quantity; -
$s_t$ : ending inventory; -
$y_t\in{0,1}$ : whether production is set up.
Assume nonnegative demand and nonnegative setup, production, and holding costs.
The Wagner-Whitin dynamic program in Sections 6.3-6.7 relies on a cost structure for which there is no speculative incentive to produce early merely to exploit a lower unit production cost. A standard sufficient condition is
Constant unit production cost is an important special case and satisfies this condition whenever holding costs are nonnegative. Under this cost structure, an optimal solution can be chosen to satisfy the zero-inventory ordering property used below [6,7].
The standard formulation is
subject to
with
Because the problem is uncapacitated, a valid and much tighter choice than an arbitrary large
ULS can be interpreted as a fixed-charge network-flow problem.
The production decisions correspond to fixed-charge arcs, while inventory carries material from one period to the next.
In the network below:
- an arc from node
$0$ to period$t$ represents production$x_t$ in period$t$ ; - opening that production arc incurs the fixed setup cost
$f_t$ ; - flow on the production arc incurs unit production cost
$p_t$ ; - an arc from period
$t$ to period$t+1$ represents inventory carried forward and incurs holding cost$h_t$ ; - the outgoing demand arc at period
$t$ represents demand$d_t$ .
Therefore the problem can be viewed in two steps conceptually:
choose which production arcs are opened
→ determine the production periods
then
→ send a minimum-cost feasible flow through the resulting network
Uncapacitated lot-sizing network for n = 4.
Under the Wagner-Whitin cost structure stated above, there exists an optimal solution satisfying
In words:
If production starts in period
$t$ , there is an optimal solution in which the inventory entering period$t$ is zero.
This is often called the zero-inventory ordering property.
Consequently, when
the quantity produced in period
for some
This is the structural property that makes the Wagner-Whitin dynamic program possible [6].
Suppose production in period
The lot size is
Its direct original cost is
The first term is the setup cost.
The second term is the production cost.
The final term is the inventory cost: demand for later periods must be held from period
Let
be the minimum original cost for satisfying demands in periods
Set
If the last production lot begins in period
Therefore,
There are
With the lot costs precomputed in
This is the classical Wagner-Whitin dynamic program [6].
An equivalent transformed objective is also useful.
From the inventory-balance equations,
Define
and
Then
The second term is constant.
So minimizing the original objective is equivalent to minimizing
up to the constant
Under the zero-inventory ordering property, if production starts at
Define
Then
and
For each prefix, define
The corresponding original-cost value is
For the full horizon,
and therefore
This distinction is important:
The task is to determine the minimum total cost and recover an optimal production and setup schedule over the four-period planning horizon.
Consider
with demands
unit production costs
holding costs
and setup costs
The cumulative holding-cost terms are
so
For the transformed recurrence:
For
For
For
The minimizing predecessor for period
Therefore the production plan is
and
The ending inventories are
The constant is
Therefore the original objective value is
We can verify it directly:
and
Thus
def wagner_whitin(demand, setup, production_cost, holding_cost):
n = len(demand)
if not (
len(setup) == n
and len(production_cost) == n
and len(holding_cost) == n
):
raise ValueError("All input arrays must have the same length.")
for t in range(n - 1):
if production_cost[t] + holding_cost[t] < production_cost[t + 1]:
raise ValueError(
"The Wagner-Whitin recurrence used here assumes "
"p_t + h_t >= p_{t+1}."
)
lot_cost = [[0] * n for _ in range(n)]
for t in range(n):
quantity = 0
holding = 0
cumulative_holding = 0
for k in range(t, n):
quantity += demand[k]
if k > t:
cumulative_holding += holding_cost[k - 1]
holding += demand[k] * cumulative_holding
lot_cost[t][k] = (
setup[t]
+ production_cost[t] * quantity
+ holding
)
dp = [float("inf")] * (n + 1)
predecessor = [None] * (n + 1)
dp[0] = 0
for k in range(1, n + 1):
for t in range(1, k + 1):
candidate = dp[t - 1] + lot_cost[t - 1][k - 1]
if candidate < dp[k]:
dp[k] = candidate
predecessor[k] = t
production = [0] * n
setup_decision = [0] * n
k = n
while k > 0:
t = predecessor[k]
setup_decision[t - 1] = 1
production[t - 1] = sum(demand[t - 1:k])
k = t - 1
return dp[n], production, setup_decisionFor the numerical example,
demand = [2, 4, 5, 1]
setup = [12, 20, 16, 8]
production_cost = [3, 3, 3, 3]
holding_cost = [1, 2, 1, 1]the function returns the original optimal cost
69
with production plan
[6, 0, 6, 0]
and setup decisions
[1, 0, 1, 0]
After the examples above, I think the most useful way to understand dynamic programming is:
state
→ summarizes all relevant information from the past
decision
→ moves from one state to another
Bellman recurrence
→ keeps only the best value reaching each state
table / memoization
→ prevents repeated solution of the same state
In many discrete optimization problems, this can also be interpreted as a path problem on an implicit state-space graph.
This is why shortest path, knapsack, and lot-sizing can all be discussed using almost the same logic.
A useful complexity rule is
The difficult part is often not writing the recurrence.
The difficult part is choosing a state that is:
- rich enough to preserve the no-aftereffect property;
- small enough that the state space remains computationally manageable.
- Dynamic programming solves a multi-stage problem by storing and reusing optimal values of subproblems.
- The principle of optimality means that the relevant continuation of an optimal solution must itself be optimal.
- A good state is a sufficient summary of the past: once the state is known, the future should not require the entire decision history.
- Overlapping subproblems explain why memoization and tabulation can save large amounts of repeated computation, but DP is not automatically polynomial-time.
- Shortest paths on a DAG can be solved by dynamic programming in
$O(|V|+|A|)$ time. - The 0-1 knapsack recurrence uses
$F_{k-1}(\lambda-a_k)$ because each item can be used at most once. - The unbounded knapsack recurrence uses
$G_k(\lambda-a_k)$ because the same item type may be used again. - Both standard knapsack dynamic programs run in
$O(nB)$ time, which is pseudo-polynomial rather than polynomial in the binary input length. - Integer knapsack can be interpreted as a longest-path problem on a DAG whose nodes represent capacity states.
- The Wagner-Whitin algorithm uses the zero-inventory ordering property to solve uncapacitated lot-sizing in
$O(n^2)$ time. - In many optimization problems, dynamic programming can be understood as shortest or longest path on an implicit state-space network.
-
Sun, Xiaoling, and Duan Li. Integer Programming. Beijing: Science Press, 2010. ISBN: 978-7-03-029380-0.(孙小玲、李端:《整数规划》,北京:科学出版社,2010年,ISBN:978-7-03-029380-0)
-
Bellman, R. Dynamic Programming. Princeton University Press, 1957. ISBN:
978-0-691-07951-6. -
Ahuja, R. K., Magnanti, T. L., and Orlin, J. B. Network Flows: Theory, Algorithms, and Applications. Prentice Hall, 1993. ISBN:
978-0-13-617549-0. -
Kellerer, H., Pferschy, U., and Pisinger, D. Knapsack Problems. Springer, 2004. DOI:
10.1007/978-3-540-24777-7. -
Agrawal, M., Kayal, N., and Saxena, N. “PRIMES is in P.” Annals of Mathematics, 160(2), 2004, pp. 781–793. DOI:
10.4007/annals.2004.160.781. -
Wagner, H. M., and Whitin, T. M. “Dynamic Version of the Economic Lot Size Model.” Management Science, 5(1), 1958, pp. 89–96. DOI:
10.1287/mnsc.5.1.89. -
Federgruen, A., and Tzur, M. “A Simple Forward Algorithm to Solve General Dynamic Lot Sizing Models with n Periods in O(n log n) or O(n) Time.” Management Science, 37(8), 1991, pp. 909–925. DOI:
10.1287/mnsc.37.8.909.
The most relevant next topics are:
-
Advanced Knapsack Problems
Builds on the knapsack dynamic-programming methods introduced in this note, with deeper study of structural properties, exact algorithms, bounded and multiple knapsack variants, and approximation methods. -
Assignment Problem
A useful example of a structured integer optimization problem that admits polynomial-time algorithms despite having a binary integer programming formulation. -
Resource-Constrained Shortest Path Problems
Extends the ordinary shortest-path problem by introducing additional resource constraints, leading naturally to richer state definitions, labeling algorithms, and dynamic-programming techniques. -
Approximation Algorithms
Especially relevant for NP-hard problems such as knapsack, where exact polynomial-time algorithms may not exist but solutions with provable approximation guarantees can still be obtained. -
Stochastic and Approximate Dynamic Programming
Extends classical dynamic programming to problems with uncertainty or very large state spaces, where exact tabulation becomes impractical and value functions may need to be approximated.



