Dynamic Programming
(DP)
Simpler, Better, Faster
Carl Hultquist
Rough definition
• Dynamic programming is:
• Breaking a problem up into smaller subproblems
• By finding the optimal solution to these
smaller sub-problems, being able to find the
solution to the bigger problem
• Dynamic programming is not:
• A type of programming language (like
declarative and imperative)
Learn by example:
Fibonacci numbers
• We all know the Fibonacci
numbers
1
1
2
3
5
8
13…
and that we can write a neat
definition for these as:
f(0) = 1
f(1) = 1
f(n) = f(n – 2) + f(n – 1) for n>1
Fibonacci in code
int f(int n)
{
if (n < 2)
return 1;
else
return f(n – 2) + f(n – 1);
}
Fibonacci and big-O
• So we have an algorithm for
finding f(n), but is it any good?
What’s the big-O for finding f(n)?
Answer:
O(f(n))
So… is this good? Is it bad?
Recursive Fibonacci:
bad
• As an indication,
f(1000)=1,318,412,525
• Surely we can do better…
The trick: overlapping
subproblems
• For some n, f(n)=f(n-2)+f(n-1)
Now f(n-1)=f(n-3)+f(n-2)
Note the common f(n-2) – so to
calculate the value of f(n), we
actually calculate f(n-2) twice. Doing
this using our recursive program is
wasteful – we should only need to
work it out once!!!
A better Fibonacci
#define MAX_N 10000
int f[MAX_N];
int fib(int n)
{
f[0] = f[1] = 1;
for (int i = 2; i <= n; i++)
f[i] = f[i – 2] + f[i – 1];
return f[n];
}
Coin counting
• Yes, today’s problem was a DP. So we want
to make change of value M, we have N coin
denominations, and their values are Vi for
i=1…N. Now we can write the solution like
this:
coins(M) = min{coins(M – V1), coins(M – V2),
…, coins(M – VN)} + 1
Now, doesn’t that look just a little bit like the
Fibonacci definition? ;-)
A better solution…
int N, M;
int V[N];
int coins[M + 1];
coins[0] = 0;
for (int i = 1; i <= M; i++)
{
int best = M;
for (int j = 0; j < N; j++)
if (V[j] <= i && coins[i – V[j]] + 1 < best)
best = coins[i – V[j]] + 1;
coins[i] = best;
}
Checking for valid
states
int N, M;
int V[N];
int coins[M + 1];
set(coins[0], coins[M], -1);
coins[0] = 0;
for (int i = 1; i <= M; i++)
{
int best = M;
for (int j = 0; j < N; j++)
if (V[j] <= i && coins[i – V[j]] != -1 && coins[i – V[j]] +
1 < best)
best = coins[i – V[j]] + 1;
coins[i] = best;
}
An alternative coin
solution: memoization
int N, M;
int V[N];
int cache[M + 1];
set(cache[0], cache[M], -1);
int coins(int amount)
{
if (cache[amount] == -1)
{
int best = M;
for (int i = 0; i < N; i++)
if (V[i] <= amount && coins(amount) + 1 < best)
best = coins(amount) + 1;
cache[amount] = best;
}
return cache[amount];
}
Two approaches to DP
• Top-down: recursion + memoization.
Easier to code, but may have greater
memory requirements. Also has
extra stack + function call overhead.
• Bottom-up: iterative, solves
subproblems from the smallest one
up. Sometimes harder to code and/or
work out exactly what’s going on,
but more efficient.
Backtracking
• Some DP problems just call for
the value of the best answer
(like today’s problem).
• Others make your life harder:
they ask for the “path” to the
solution.
• Today’s problem could have done
this by asking you to output the
actual coins used
Backtracking (2)
• This usually isn’t too hard: in the
same way that you have an array to
store your best solution, you also
keep an array indicating how you got
there.
• For the coins problem, this array can
simply store the last coin used to reach
each total
• You can then “backtrack” by subtracting
the coin value from the total, and look at
the next coin that was needed. Repeat
until you hit 0
Coins with backtrack
int
int
int
int
N, M;
V[N];
coins[M + 1];
coinUsed[M + 1];
coins[0] = 0;
for (int i = 1; i <= M; i++)
{
int best = M;
int coin = -1;
for (int j = 0; j < N; j++)
if (V[j] <= i && coins[i – V[j]] + 1 < best)
{
best = coins[i – V[j]] + 1;
coin = j;
}
coins[i] = best;
coinUsed[i] = coin;
}
Higher dimensions
• So far, we’ve just seen 1D DP
problems
• At the IOI, 2D (or higher!) problems
crop up often
• Consider this one: given a NxN grid
of numbers and M co-ordinate pairs
(a,b);(c,d), find the sum of the values
in the grid for each of the rectangles
with top-left co-ordinate (a,b) and
bottom-right co-ordinate (c,d).
The naïve approach
• For each of the M rectangles,
loop over the rectangle and sum
up the values. This has O(N2M).
• With DP, we can instead get
O(N2+M) which will usually be
better (unless M is huge)
Getting clever…
• Suppose we can compute and store
the sum of all rectangles with topleft co-ordinate (1,1). Let’s stash
these in a 2D array called sum[][].
• Then, to work out the sum of a
rectangle from (a,b) to (c,d), we can
work it out using our sum array like
this:
sum(a,b,c,d) = sum[c][d] – sum[c][b-1] –
sum[a-1][d] + sum[a-1][b-1]
Cool, now how do we
create our sum array?
• Suppose the grid values are in an
array called grid[][]. To work out
sum[i][j], we just need to see that it
can be calculated as:
sum[i][i] =
sum[i][j-1] +
sum[i-1][j] –
sum[i-1][j-1] +
grid[i][j]
… which we can do with
a quick 2D loop
sum[0][0] = 0;
sum[0][j] = 0; sum[i][0] = 0;
for (int i = 1; i <= N; i++)
for (int j = 1; j <= N; j++)
sum[i][j] =
sum[i-1][j] +
sum[i][j-1] sum[i-1][j-1] +
grid[i][j];
Common applications
• Longest common subsequence
problem
• Floyd’s all-pairs shortest path
• Knapsack problem
• Duckworth-Lewis method!
… amongst many others. See:
http://en.wikipedia.org/wiki/Dynamic_Programming
Questions?
© Copyright 2026 Paperzz