动态规划

Intro: The Rod-Cutting Problem

Given a rod of length nn inches and a table of prices pip_i for i=1,2,,ni = 1, 2, \ldots, n, determine the maximum revenue rnr_n obtainable by cutting up the rod and selling the pieces.

There are 2n12^{n-1} ways to cut up a length nn rod. So enumerating all possibilities is not a good idea.

2n12^{n-1} 实际上是上界,这个问题实际上更复杂一点,参见整数分拆

Optimal structure property: rn=max1in(pi+rni)r_n = \max\limits_{1 \le i \le n}(p_i + r_{n-i}).

The greedy choice property(Always cut at most profitable position maxpii\max \dfrac{p_i}{i}) doesn't hold. For example n=3,p1=1,p2=7,p3=9n = 3, p_1 = 1, p_2 = 7, p_3 = 9, the optimal solution is not to cut.

A simple recursive algorithm:

1
2
3
4
5
6
7
CutRodRec(prices, n):
if n = 0
return 0
r := -INF
for i := 1 to n
r := max(r, prices[i] + CutRodRec(prices, n - i))
return r

This algorithm actually enumerates all 2n12^{n-1} possibilities:

For each subproblem, only need to solve it once. Each node denotes a subproblem of certain size. Some subproblems appear multiple times:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CutRodRecMem(prices, n):
for i := 0 to n
r[i] := -INF
return CutRodRecMemAux(prices, r, n)

CutRodRecMemAux(prices, r, n):
if r[n] >= 0
return r[n]
if n = 0
q := 0
else
q := -INF
for i := 1 to n
q := max(q, prices[i] + CutRodRecMemAux(prices, r, n - i))
r[n] := q
return q

Runtime: Each subproblem is solved once. Solving size ii problem itself without subproblems needs Θ(i)\Theta(i) time. Thus total runtime is Θ(1++n)=Θ(n2)\Theta(1 + \dots + n) = \Theta(n^2).

The result of subproblem 4 is unknown if subproblems 0, …, 3 is not finished. Therefore, these relationship can be represented as a DAG:

Then, we can use DFS to get the topological order of the DAG, and solve the subproblems in this order:

1
2
3
4
5
6
7
8
CutRodIter(prices, n):
r[0] := 0
for i := 1 to n
q := -INF
for j := 1 to i
q := max(q, prices[j] + r[i - j])
r[i] := q
return r[n]

Runtime is still Θ(n2)\Theta(n^2).

This outputs the optimal revenue without the actual cutting plan. To get the cutting plan, we can store the cut position for each subproblem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CutRodIter(prices, n):
r[0] := 0
for i := 1 to n
q := -INF
for j := 1 to i
if q < prices[j] + r[i - j]
q := prices[j] + r[i - j]
cuts[i] := j
r[i] := q
return r[n]

PrintOpt(cuts, n):
while n > 0
Print cuts[n]
n := n - cuts[n]

Dynamic Programming(DP) Concept

An optimal problem:

  • Build optimal solution step by step.
  • Problem has optimal substructure property.
    • We can design a recursive algorithm.
  • Problem has lots of overlapping subproblems.
    • Recursion and memorize solutions. (Top-Down)
    • Or, consider subproblems in the right order. (Bottom-Up)

The Floyd-Warshall Algorithm is a example of DP via bottom-up approach.

Procedure of developing a DP algorithm:

  1. Characterize the structure if solution.
    • e.g., (one cut of length ii) + (solution for length nin-i)
  2. Recursively define the value of an optimal solution.
    • e.g., rn=max1in(pi+rni)r_n = \max\limits_{1\le i\le n} (p_i + r_{n-i})
  3. Compute the value of an optimal solution.
    • Top-down or Bottom-up.
    • Usually bottom-up is more efficient.
  4. (Optional) Construct an optimal solution
    • Remember optimal choices beside optimal solution values.

Examples

Matrix-Chain Multiplication

  • Input: Matrices A1,,AnA_1, \dots, A_n with size pi1×pip_{i-1} \times p_i
  • Output: A1AnA_1 \cdots A_n
  • Problem: Compute output with minimum work.

Since matrix multiplication is associative, the order of multiplication doesn't matter in terms of the result. But the number of scalar multiplications can be different.

For example, A1A2A3A_1A_2A_3 can be computed in two ways:

  • (A1A2)A3(A_1A_2)A_3: p0p1p2+p0p2p3p_0p_1p_2 + p_0p_2p_3 multiplications
  • A1(A2A3)A_1(A_2A_3): p1p2p3+p0p1p3p_1p_2p_3 + p_0p_1p_3 multiplications

If p0=10,p1=100,p2=5,p3=50p_0 = 10, p_1 = 100, p_2 = 5, p_3 = 50, then The number of the seconds multiplications is ten times of the first one.

Procedure:

  1. Characterize the structure of an optimal solution.
    • For every order, the last step is (A1Ak)(Ak+1An)(A_1 \cdots A_{k}) \cdot (A_{k+1} \cdots A_n)
    • In general, AiAj=(AiAk)(Ak+1Aj)A_i \cdots A_{j} = (A_i \cdots A_{k}) \cdot (A_{k+1} \cdots A_{j})
  2. Recursively define the value of an optimal solution.
    • Let m[i,j]m[i, j] be the minimal cost for computing AiAjA_i \cdots A_j
    • m[i,j]=minik<j(m[i,k]+m[k+1,j]+pi1pkpj)m[i, j] = \min\limits_{i \le k < j} (m[i, k] + m[k+1, j] + p_{i-1}p_kp_j)

To get the bottom-up method, we have to find out the dependency of m[i,j]m[i, j]. Actually, m[i,j]m[i, j] depends on m[i,j]m[i', j'] where iijji \le i' \le j' \le j.

Then we can compute m[i,j]m[i, j] in length increasing order:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MatrixChainDP(A1, ..., An):
for i := 1 to n
m[i, i] := 0
for l := 2 to n
for i := 1 to n - l + 1
j := i + l - 1
m[i, j] := INF
for k := i to j - 1
cost := m[i, k] + m[k+1, j] + p[i-1] * p[k] * p[j]
if cost < m[i, j]
m[i, j] := cost
s[i, j] := k
return m, s

MatrixChainPrintOpt(s, i, j):
if i = j
Print "A" + i
else
Print "("
MatrixChainPrintOpt(s, i, s[i, j])
MatrixChainPrintOpt(s, s[i, j] + 1, j)
Print ")"

Edit Distance

Given two strings, how similar are they?

Here are following three types of operations for a string:

  1. Insertion: Insert a character at a position.
  2. Deletion: Remove a character at a position.
  3. Substitution: Change a character to another one.

Edit Distance of string A,BA, B is the minimal number of ops to transform AA into BB.

Example of transforming SNOWY to SUNNY:

  1. SNOWY \to SUNOWY (Insert 'U' at position 1)
  2. SUNOWY \to SUNOY (Delete 'W' at position 4)
  3. SUNOY \to SUNNY (Substitute 'O' with 'N' at position 3)

It's edit distance is at most 3 and it is 3 actually.

One way to visualize the editing process:

  1. Align AA above BB
  2. A gap in the first line indicates an insertion to AA
  3. A gap in the second line indicates a deletion from AA
  4. A column with different characters indicates a substitution.

Consider transform A[1m]A[1\dots m] to B[1n]B[1\dots n]. Each solution can be visualized in the way described above.

Last column must be one of three cases:

  • /B[n]- / B[n]
  • A[m]/B[n]A[m] / B[n]
  • A[m]/A[m] / -

Each case reduces the problem to a subproblem:

  • /B[n]- / B[n]: Edit distance of A[1m]A[1\dots m] and B[1(n1)]B[1\dots (n-1)]
  • A[m]/B[n]A[m] / B[n]: Edit distance of A[1(m1)]A[1\dots (m-1)] and B[1(n1)]B[1\dots (n-1)]
  • A[m]/A[m] / -: Edit distance of A[1(m1)]A[1\dots (m-1)] and B[1n]B[1\dots n]

Then we get recursive optimal solution:

dist(i,j)={i,if j=0j,if i=0min{dist(i1,j)+1,dist(i,j1)+1,dist(i1,j1)+I(A[i]B[j])},otherwise\operatorname{dist}(i, j) = \begin{cases} i, & \text{if } j = 0 \\ j, & \text{if } i = 0 \\ \min\left\lbrace \begin{aligned} & \operatorname{dist}(i-1, j) + 1, \\ & \operatorname{dist}(i, j-1) + 1, \\ & \operatorname{dist}(i-1, j-1) + \mathbb{I}(A[i] \ne B[j]) \end{aligned} \right\rbrace, & \text{otherwise} \end{cases}

dist(i,j)\operatorname{dist}(i, j) depends on dist(i1,j)\operatorname{dist}(i-1, j), dist(i,j1)\operatorname{dist}(i, j-1), and dist(i1,j1)\operatorname{dist}(i-1, j-1).

Then we can compute the edit distance in a bottom-up way: In an outer loop, increase ii; in an inner loop, increase jj.

1
2
3
4
5
6
7
8
9
10
11
12
EditDistDP(A[1...m], B[1...n]):
for i := 0 to m
dist[i, 0] := i
for j := 0 to n
dist[0, j] := j
for i := 1 to m
for j := 1 to n
delDist := dist[i-1, j] + 1
insDist := dist[i, j-1] + 1
subDist := dist[i-1, j-1] + bool(A[i] != B[j])
dist[i, j] := min(delDist, insDist, subDist)
return dist

A process example:

Maximum Independent Set

Given an undirected graph G=V,EG = \left\langle V, E \right\rangle, an independent set(独立集) II is a subset of VV such that no vertices in II are adjacent. Put another way, for all (u,v)I×I(u, v) \in I \times I, we have (u,v)E(u, v) \notin E.

A maximum independent set (MaxIS) is an independent set with the maximum number of vertices.

Computing MaxIS in an arbitrary graph is NP-head. Even getting an approximate MaxIS is very hard.

But if the graph is a tree, we can compute MaxIS in polynomial time.

Given an IS II of TT, for each child uu of rr, set IV(Tu)I \cap V(T_u) is an IS of TuT_u.

Let mis(Tu)\operatorname{mis}(T_u) be size of MaxIS of subtree rooted at node uu, and:

  • mis(Tu,1)\operatorname{mis}(T_u, 1) be size of MaxIS of TuT_u s.t. uu in the MaxIS.
  • mis(Tu,0)\operatorname{mis}(T_u, 0) be size of MaxIS of TuT_u s.t. uu not in the MaxIS.

Then we have:

  • mis(Tu,1)=1+v is a child of umis(Tv,0)\operatorname{mis}(T_u, 1) = 1 + \displaystyle \sum_{v \text{ is a child of } u} \operatorname{mis}(T_v, 0)
  • mis(Tu,0)=v is a child of umis(Tv)\operatorname{mis}(T_u, 0) = \displaystyle \sum_{v \text{ is a child of } u} \operatorname{mis}(T_v)
  • mis(Tu)=max(mis(Tu,0),mis(Tu,1))\operatorname{mis}(T_u) = \max(\operatorname{mis}(T_u, 0), \operatorname{mis}(T_u, 1))

1
2
3
4
5
6
7
8
MaxIsDP(u):
mis1 := 1
mis0 := 0
for each child v of u
mis1 := mis1 + MaxIsDP(v, 0).mis0
mis0 := mis0 + MaxIsDP(v).mis
mis := max(mis0, mis1)
return mis, mis0, mis1

Runtime is O(V+E)=O(V)O(V + E) = O(V).

Discussions of DP

Optimal Substructure Property

DP requires the optimal substructure property. If this doesn't hold, we can't use DP, so do greedy algorithms.

Here's examples that optimal substructure property holds and doesn't hold:

Top-Down v.s. Bottom-Up

Top-down: Recursion with memorization.

  • Very straightforward, easy to write down the code.
  • Use array or hash-table to memorize
    solutions.
  • Array may cost more space, but hash-table may cost more time.

Bottom-up: Solve subproblems in the right order.

  • Finding the right order might be non-trivial.
  • Usually use array to memorize solutions.
  • Might be able to reduce the size of array to save even more space.

Top-down often costs more time in practice cos recursion is costly. But this is not always true since Top-down only considers necessary subproblems.

APSP via DP

dist[*, *, r] only relies on dist[*, *, r-1]. So we can repeatedly use dist array to reduce space.

Edit Distance

Same as APSP.

Analysis of DP

Correctness:

  • Optimal substructure property.
  • Bottom-up approach: subproblems are already solved.

Complexity:

  • Space complexity: usually obvious.
  • Time complexity [bottom-up]: usually obvious since iterations number is known.
  • Time complexity [top-down]: Master theorem.
    • How many subproblems in total? (number of nodes in the subproblem DAG)
    • Time to solve a problem, given subproblem solutions? (number of edges in the subproblem DAG)

Subset Sum

DP can not only work in optimization problems.

  • Problem: Given an array X[1n]X[1\dots n] of n positive integers, can we find a subset that sums to given integer TT?

Simple solution: Recursively enumerates all 2n2^n subsets, leading to O(2n)O(2^n) runtime.

Observation:

  1. If there is a solution SS, either X[1]X[1] is in it or not
  2. If X[1]X[1] in SS, then there is a solution to instance X[2n]X[2\dots n] and TX[1]T - X[1]
  3. If X[1]X[1] not in SS, then there is a solution to instance X[2n]X[2\dots n] and TT

Let ss(i,t)\operatorname{ss}(i, t) be true if there is a subset of X[1i]X[1\dots i] that sums to tt.

Then we have:

ss(i,t)={true,if t=0false,if i>nss(i+1,t),if X[i]>tss(i+1,t)ss(i+1,tX[i]),otherwise\operatorname{ss}(i, t) = \begin{cases} \text{true}, & \text{if } t = 0 \\ \text{false}, & \text{if } i > n \operatorname{ss}(i + 1, t), & \text{if } X[i] > t \\ \operatorname{ss}(i + 1, t) \lor \operatorname{ss}(i + 1, t - X[i]), & \text{otherwise} \end{cases}

1
2
3
4
5
6
7
8
9
10
11
SubsetSumDP(X[1...n], T):
ss[n, 0] := true
for t := 1 to T
ss[n, t] := (X[n] = t)
for i := n - 1 down to 1
ss[i, 0] := true
for t := 1 to X[i] - 1
ss[i, t] := ss[i + 1, t]
for t := X[i] to T
ss[i, t] := ss[i + 1, t] || ss[i + 1, t - X[i]]
return ss

Runtime is O(nT)O(n T), depending on TT. Thus DP isn't always an improvement.

DP v.s. Greedy

Dynamic Programming

  • At each step: multiple potential choices, each reducing the problem to a subproblem, compute optimal solutions of all subproblems and then find optimal solution of original problem.
  • Optimal substructure + Overlapping subproblems.

Greedy

  • At each step: make an optimal choice, then compute optimal solution of the subproblem induced by the choice made.
  • Optimal substructure + Greedy choice