4 - Dynamic Programming#
Format of dynamic programming algorithms:
Use a recursive formula
But recursion can be inefficient because it might repeat the same computations multiple times
So use an array to store and lookup the subproblem results as needed
Fibonacci Numbers#
\(F(0) = 1\)
\(F(1) = 1\)
\(F(n) = F(n - 2) + F(n - 1)\), when \(n \geq 2\)
Recursive (Naive) Approach#
int F(int n) {
if (n == 0 || n == 1)
return 1
else
return F(n-2) + F(n-1)
}
Recursion as in the example above yields repeated subproblems, so its runtime is exponential.
Bottom-Up Approach (Table, No Recursion)#
Allocate array A[0..n]
for (k = 0; k <= n; k++)
if (k == 0 || k == 1)
A[k] = 1
else
A[k] = A[k-2] + A[k-1]
return A[n]
Runtime: \(\Theta(n)\)
Top-Down Approach (Table and Recursion)#
Allocate array A[0..n]
for (k = 0; k <= n; k++)
A[k] = null
return F(n)
int F(int n) {
if (A[n] != null)
return A[n]
if (n == 0 || n == 1)
A[n] = 1
else
A[n] = F(n-2) + F(n-1)
return A[n]
}
Runtime: \(\Theta(n)\)
Combinations#
Pascal’s recursive formula for combinations:
C(n, 0) = 1
C(n, n) = 1
C(n, k) = C(n-1, k-1) + C(n-1, k), when 0 < k < n
int C(int n, int k) {
if (k == 0 || k == n)
return 1
else
return C(n-1, k-1) + C(n-1, k)
}
Again, recursion yields repeated subproblems, so runtime is exponential.
Pascal’s Triangle (DP Approach)#
Allocate array C[0..n][0..k]
for(m=0; m<=n; m++)
for(j=0; j<=k; j++)
if (j == 0 || j == m)
C[m][j] = 1
else
C[m][j] = C[m-1][j-1] + C[m-1][j]
return C[n][k]
Runtime: \(\Theta(nk) = \Theta(n^2)\), because \(k \leq n\)
0-1 Knapsack Problem#
Objects \(\{ 1 \dots n \}\)
Profits \(P[ 1 \dots n ]\)
Weights \(W[ 1 \dots n ]\)
Maximum weight capacity \(M\)
0-1 Constraint: Must take none (0) or all (1) of each of object.
Goal: Determine the amounts \(X[1 \dots n]\) for each object so that \(\sum_{1 \leq k \leq n} X[k] * W[k] \leq M\), and \(\sum_{1 \leq k \leq n} X[k] * P[k]\) is as large as possible. Each \(X[j]\) must be either \(0\) or \(1\).
Recursive Function#
\(T(n, M)\) is the max possible total profit such that we choose any subset of objects \(\{ 1 \dots n \}\) and maximum weight capacity is \(M\).
\(T(j, k)\) is the max possible total profit such that we can only choose from objects \(\{ 1 \dots j \}\) and maximum weight capacity is \(k\).
\((0 \leq j \leq n, 0 \leq k \leq M)\)
Formulation:
\(T(j, k) = 0\), if \(j == 0\)
\(T(j, k) = T(j-1, k)\), if \(j > 0\) and \(k < W[j]\)
\(T(j, k) = \max \{ T(j-1, k), T(j-1, k-W[j]) + P[j] \}\) if \(j > 0\) and \(k \geq W[j]\)
int T(int j, int k) {
if (j == 0)
return 0
if (k < W[j])
return T(j-1, k)
return max{T(j-1, k), T(j-1, k-W[j]) + P[j]}
}
Dynamic Programming Algorithm#
Allocate array T[0..n][0..M]
for j = 0 to n
for k = 0 to M
if (j == 0)
T[j][k] = 0
else if (k < W[j])
T[j][k] = T[j-1][k]
else
T[j][k] = max{T[j-1][k], T[j-1][k-W[j]] + P[j]}
Runtime: \(\Theta(nM)\)
Example#
\(n = 4\)
\(M = 8\)
1 |
2 |
3 |
4 |
|
---|---|---|---|---|
P |
12 |
15 |
16 |
18 |
W |
2 |
3 |
4 |
6 |
T |
k=0 |
k=1 |
k=2 |
k=3 |
k=4 |
k=5 |
k=6 |
k=7 |
k=8 |
---|---|---|---|---|---|---|---|---|---|
\(j=0\) |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
\(j=1\) |
0 |
0 |
12 |
12 |
12 |
12 |
12 |
12 |
12 |
\(j=2\) |
0 |
0 |
12 |
15 |
15 |
27 |
27 |
27 |
27 |
\(j=3\) |
0 |
0 |
12 |
15 |
16 |
27 |
28 |
31 |
31 |
\(j=4\) |
0 |
0 |
12 |
15 |
16 |
27 |
28 |
31 |
31 |
So the max total profit is 31, but which objects do we need to choose? We still need to assign each \(X[j]\) with either 0 or 1.
k = M
for (j=n; j>0; j--)
if (T[j-1][k] == T[j][k])
X[j] = 0
else
{ X[j] = 1 }
{ k -= W[j] }
This takes \(\Theta(n)\) additional time.
Longest Common Subsequence (LCS)#
Given two strings:
\(X[1 \dots m]\)
\(Y[1 \dots n]\)
Goal: Find a string that is a subsequence of both \(X\) and \(Y\), and that has the largest possible length.
Example:
\(X=\) “bdacbea”, \(m=7\)
\(Y=\) “dcbaecba”, \(n=8\)
\(\text{LCS}(X, Y) =\) “dacba”
Recursive Function for LCS#
\(\text{L}(m, n) =\) length of the LCS of \(X[1 \dots m]\) and \(Y[1 \dots n]\)
\(\text{L}(j, k) =\) length of the LCS of \(X[1 \dots j]\) and \(Y[1 \dots k] (0 \leq j \leq m, 0 \leq k \leq n)\)
Formulation:
\(\text{L}(m, n) = 0\) if \(j = 0\) or \(k = 0\)
\(\text{L}(m, n) = \text{L}(j-1, k-1) + 1\) if \(j > 0, k > 0,\) and \(X[j] = Y[k]\)
\(\text{L}(m, n) = \max \{ \text{L}(j-1, k), \text{L}(j, k-1) \}\) if \(j > 0, k > 0,\) and \(X[j] \ne Y[k]\)
int L(int j, int k) {
if (j == 0 || k == 0)
return 0
if (X[j] == Y[k])
return L(j-1, k-1) + 1
return max{L(j-1, k), L(j, k-1)}
}
The runtime of this function is exponential due to repeated subproblems.
Dynamic Programming Algorithm#
Allocate array L[0..m][0..n]
for j = 0 to m
for k = 0 to n
if (j == 0 || k == 0)
L[j][k] = 0
else if (X[j] == Y[k])
L[j][k] = L[j-1][k-1] + 1
else
L[j][k] = max{L[j-1][k], L[j][k-1]}
Runtime: \(\Theta(mn)\)
Example#
\(X=\) “bdacbea”, \(m=7\)
\(Y=\) “dcbaecba”, \(n=8\)
L |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
---|---|---|---|---|---|---|---|---|---|
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
2 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
3 |
0 |
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
4 |
0 |
1 |
2 |
2 |
2 |
2 |
3 |
3 |
3 |
5 |
0 |
1 |
2 |
3 |
3 |
3 |
3 |
4 |
4 |
6 |
0 |
1 |
2 |
3 |
3 |
4 |
4 |
4 |
4 |
7 |
0 |
1 |
2 |
3 |
4 |
4 |
4 |
4 |
5 |
How can we find an LCS that has length 5?
string buildLCS(j, k) {
if (L[j][k] == 0)
return ""
else if (X[j] == Y[k])
return buildLCS(j-1, k-1) + X[j]
else if (L[j][k-1] == L[j][k])
return buildLCS(j, k-1)
else
return buildLCS(j-1, k)
}
Runtime: \(\Theta(m+n)\)
Assumes we append characters in \(\Theta(1)\) time.
This is possible because we know the final string length \(\text{L}(m, n)\) in advance.
Can allocate an array of size \(\text{L}(m, n)\) chars in advance, or replace recursion by iteration.
Optimal Binary Search Tree (BST)#
Construct a BST with Key\(_1\) < Key\(_2\) < Key\(_3\) < \(\dots\) < Key\(_n\). The frequency of searching for each Key\(_i\) is \(\text{F}[i]\).
Example: Key\([1..3] = \{ a, b, c \}\) and \(\text{F}[1..3] = \{ 0.3, 0.5, 0.2 \}\).
Goal: Construct the BST with the minimum expected search cost.
Let \(\text{Cost}[i][j]\) be the minimum expected search cost for a BST with Key\([i..j]\).
Find the best choice Key\([r]\) for the root node.
Left subtree has Key\([i..r-1]\).
Right subtree has Key\([r+1..j]\)
Formulation:
\(\text{Cost}[i][j] = 0\) when \(i > j\)
\(\text{Cost}[i][j] = \text{F}[i]\) when \(i = j\)
\(\text{Cost}[i][j] = \min \{ \text{Cost}[i][r-1] + \text{Cost}[r+1][j] + \sum_{i \leq k \leq j} \text{F}[k] \ \ | \ \ i \leq r \leq j \}\) when \(i < j\)
For efficiency, define \(\text{W}[i][j] = \sum_{i \leq k \leq j} \text{F}[k]\)
\(\text{Cost}[i][j] = \min \{ \text{Cost}[i][r-1] + \text{Cost}[r+1][j] + \text{W}[i][j] \ \ | \ \ i \leq r \leq j \}\) when \(i < j\)
Dynamic Programming Algorithm#
Allocate arrays Cost[1..n+1][0..n], W[1..n+1][0..n], and Root[1..n+1][0..n]
for i = 0 to n
Cost[i+1][i] = 0
W[i+1][i] = 0
for L = 1 to n
for i = 1 to n-L+1
j = i+L-1
Cost[i][j] = infinity
W[i][j] = W[i][j-1] F[j]
for r = i to j
t = Cost[i][r-1] + Cost[r+1][j] + W[i][j]
if (t < Cost[i][j])
Cost[i][j] = t
Root[i][j] = r
Runtime: \(\Theta(n^3)\)
Example: \(n=3, \text{F}[1..3] = \{ 0.3, 0.5, 0.2 \}\)
Next build the BST to achieve the minimum expected search cost.
BuildBST(i, j) {
if (i > j)
return null
r = Root[i][j]
left = BuildBST(i, r-1)
right = BuildBST(r+1, j)
return new Node(Key[r], left, right)
}
tree = BuildBST(1, n)
All-Pairs Shortest Paths#
Given a weighted (undirected or directed) graph, find a minimum-distance path from each start vertex to each destination vertex.
Restriction: We allow edges with negative weights, but not any cycle with negative total weight.
Floyd’s Algorithm#
// initialize D0 = weighted adjacency matrix
for X = 1 to n
for Y = 1 to n
if (X == Y)
D0[X, Y] = 0
else if (edge (X, Y) exists)
D0[X, Y] = weight(X, Y)
else
D0[X, Y] = infinity
// compute each Dz matrix values in the D_{z-1} matrix
for Z = 1 to n
for X = 1 to n
for Y = 1 to n
if (D_{z-1}[X, Z] + D_{z-1}[Z, Y] < D_{z-1} matrix)
Dz[X, Y] = D_{z-1}[X, Z] + D_{z-1}[Z, Y]
else
Dz[X, Y] = D_{z-1}[X, Y]
Runtime: \(\Theta(n^3)\)
Memory usage: \(\Theta(n^3)\)
The memory usage can be improved to \(\Theta(n^2)\) by letting \(D\) be a 2-D array
for X = 1 to n
for Y = 1 to n
if (X == Y)
D[X, Y] = 0
else if (edge (X, Y) exists)
D[X, Y] = weight(X, Y)
else
D[X, Y] = infinity
for Z = 1 to n
for X = 1 to n
for Y = 1 to n
if (D[X, Z] + D[Z, Y] < D[X, Y])
D[X, Y] = D[X, Z] + D[Z, Y]
To determine if the graph has a negative cycle:
boolean hasNegativeCycle() {
for X = 1 to n
if (D[X, Y] < 0)
return true
return false
}
World Series Problem#
Teams A and B compete in a series of games.
The winner is the first team to win \(n\) games.
The series ends as soon as the winner is decided.
At most, \(2n-1\) games are played.
For \(1 \leq k \leq 2n-1\), let
p[k]
be the probability that team A wins the \(k\)th game (These are the input values).For \(0 \leq i \leq n\) and \(0 \leq j \leq n\), let
X(i, j)
be the probability that the series reaches a situation where team A wins exactly \(i\) games and team B wins exactly \(j\) games (These are the output values).
First, we develop a recursive formula:
X(0, 0) = 1
X(n, n) = 0
X(i, 0) = X(i-1, 0) * p[i], for i>0
X(0, j) = X(0, j-1) * (1 - p[j]), for j>0
X(n, j) = X(n-1, j) * p[n+j], for j<n
X(i, n) = X(i, n-1) * (1 - p[i+n]), for i<n
X(i, j) = X(i-1, j) * p[i+j] + X(i, j-1) * (1 - p[i+j]), for 0<i<n and 0<j<n
Dynamic Programming Algorithm#
Allocate array X[0..n][0..n]
for (i=0; i<=n; i++)
for (j=0; j<=n; j++)
if (i == 0 && j == 0)
X[i][j] = 1
else if (i == n && j == n)
X[i][j] = 0
else if ((i > 0 && j == 0) || (i == n && j < n))
X[i][j] = X[i-1][j] * p[i+j]
else if ((i == 0 && j > 0) || (i < n && j == n))
X[i][j] = X[i][j-1] * (1 - p[i+j])
else
X[i][j] = X[i-1][j] * p[i+j] + X[i][j-1] * (1 - p[i+j])
Runtime: \(\Theta(n^2)\)
Matrix Chain Product (MCP)#
Multiply chain of compatible matrices \(M_1 M_2 M_3 \dots M_n\). Each matrix \(M_j\) has \(\text{D}[j-1]\) rows and \(\text{D}[j]\) columns.
For example, \(M_1 M_2 M_3\) where \(\text{D}[0 \dots 3] = \{ 10, 20, 5, 30 \}\). \(M_1\) is \(10 \times 20\), \(M_2\) is \(20 \times 5\), and \(M_3\) is \(5 \times 30\).
\((M_1 M_2) M_3\) takes \(10*20*5 + 10*5*30 = 2500\) scalar products.
\(M_1 (M_2 M_3)\) takes \(20*5*30 + 10*20*30 = 9000\) scalar products.
The goal is to compute \(M_1 M_2 M_3 \dots M_n\) using the fewest scalar products.
Let \(\text{Cost}[i][j]\) be the fewest scalar products to compute \(M_i \dots M_j\). Note that \(\text{Cost}[i][j] = 0\) when \(i = j\).
\(\text{Cost}[i][j] = \min \{ \text{Cost}[i][k] + \text{Cost}[k+1][j] + \text{D}[i-1]*\text{D}[k]*\text{D}[j] | i \leq k \leq j-1 \}\) when \(i < j\)
Dynamic Programming Algorithm#
MATRIX-CHAIN-PRODUCT(D)
Allocate arrays Cost[1..n][1..n] and Bestk[1..n][1..n]
for i = 1 to n
Cost[i][j] = 0
for L = 2 to n
for i = 1 to n-L+1
j = i + L - 1
Cost[i][j] = infinity
for k = i to j-1
t = Cost[i][k] + Cost[k+1][j] + D[i-1]*D[k]*D[j]
if (t < Cost[i][j])
Cost[i][j] = t
Bestk[i][j] = k
The runtime of this algorithm is \(\Theta(n^3)\)