§ Dynamic Programming: Erik Demaine's lectures


I realized I'd never bothered to ever formally learn dynamic programming, so I'm watching Erik Demaine's lectures and taking down notes here.

§ DP 1: Fibonacci, shortest paths



  1. DP ~= careful brute force.
  2. DP ~= subproblems + "recurse"

§ Fibonacci


F(1) = F(2) = 1; F(n) = F(n-1) + F(n-2)

§ Naive:


fib(n):
  if n <= 2: f = 1
  else f = fib(n-1) + fib(n-2)
  return f

EXPONENTIAL time! T(n) = T(n-1) + T(n-2) + O(1). Since it's the fibonacci recurrence, the solution is rougly ϕn\phi^n where ϕ\phi is the golden ratio. Alternate, T(n) >= 2T(n-2) ~ 2^(n/2)

§ Memoized DP:


memo = {}
fib(n):
  # vvv
  if n in memo: return memo[n]
  if n <= 2: f = 1
  else f = fib(n-1) + fib(n-2)
  # ^^^
  memo[n] = f
  return f


§ Bottom up DP algorithm


fib = {}
 -- | some thought for the loop
for k in range(1, n+1):
  if k <= 2: f = 1
  else: f = fib[k-1] + fib[k-2]
  fib[k] = f

Order based on any topo sort of dependency DAG. From the bottom up perspective, we can decide how much we need to store based on how much state we need.
  *------------*
  |            v
f(n-2) f(n-1) f(n)
          |    ^
          *----*

§ Single Source Shortest paths ( s-v path)



There is some hypothetical path from s to v. We don't know what the first edge of this hypothetical path is, so we guess. We try all of the paths from s->s'. This changes s, but we really care about single source shortest path. So rather, we choose to guess v. We guess the last edge u? -> v. Recursively compute the path from s to u?, and then add the path to v.
\delta(s, v) = \min_{(u,v) \in E} \delta(s, u) + w(u, v)

Subpaths of shortest paths are shortest paths! Memoize to make it fast? Why is it faster on memoization?
*---a----*
v   ^    v
s   |    w
|   |    |
*-->b<---*

δ(s, w)
  δ(s, a)
     δ(s, b)
       δ(s, s)
       δ(s, w) <- INFINITE


total time = sum over times of all subproblems (modulo recursion)

This gives us:
total time = sum indeg(v) + O(1) = O(E) + O(1)



Sid question : Can we derive Djikstras using the same "cycle explosion"
trick?

We define δk(s,v)\delta_k(s, v) to be weight of shortest path that uses at most k edges.
\delta_k(s, v) = \min_{(u, v) \in E} \delta_{k-1}(s, u) + w(u, v)

We've increased the number of subproblems. We know that the longest path possible can have V1|V| - 1 edges. So the k parameter goes from [0..|V|-1] while the vertex v can be any vertex. Per vertex v we spend indeg(v) time. So we get the total recurrence as:
(k[V1],vV)T(k,v)=k[V1]vindeg(v)=k[V1]E=VE \begin{aligned} &\sum_{(k \in [|V|-1], v \in V)} T(k, v) = \\ &\sum_{k \in [|V|-1]} \sum_v indeg(v) = \sum_{k \in [|V|-1]} E = VE \end{aligned}

§ DP 2: Text Justification, Blackjack



§ 5 "easy" steps to a DP


  1. Define subproblems; analysis - number of subproblems
  2. Guess (part of solution); analysis - number of choices for the guess
  3. Relate subproblem solutions [with a recurrence ]; analysis - time per subproblem (ignoring recursion)
  4. Build an algorithm: [recursion/memo, or tabling ]; check recurrence is acyclic
  5. solve original problem; total time: total time across all subproblems (ignoring recursion). In simple cases, total time = number of subproblems x time per subproblem.
  6. Check that the original problem actually gets solved!

§ Recap: Fibonacci

  1. subproblems: F(1)...F(n)
  2. guess: nothing
  3. relate: F(n) = F(n-1) + F(n-2); O(1) time
  4. F(n). constant time to find

§ Recap: Shortest path

  1. subproblems: δk(s,v)\delta_k(s, v). V2V^2 subproblems.
  2. guess: last edge; edge into vv.
  3. relate: δk(s,v)=minuδk1(s,u)+w(u,v)\delta_k(s, v) = \min_u \delta_{k-1}(s, u) + w(u, v); indegree(v) time
  4. δv1(s,v)\delta_{v-1}(s, v) for all vv. This takes Θ(V)\Theta(V).

§ Text Justification


split text into "good lines". can only cut between word boundaries. Text is a list of words. badness(i, j): how bad is it to use words[i:j] in a line. They may fit, or they may not fit. If they don't fit, then badness is . Otherwise, it's going to be (pagewidth - total width)^3. We want to minimize the sum of badnesses of the lines.
  1. subproblems: the hard part! exponential: Guess for every word, whether a line begins or not. What is the natural thing to guess? guess how long the first line is / guess where the second line begins. After I guess where the second line is, I now have the remaining words to text justify. So the subproblems are going to be suffixes of the array: words[i:]. If we have n words, we have n suffixes. We're going to only remember one line [forget the past! ], not all the lines! [this is exponential! ]
  2. Guess: where to start the second line. If we are at location i, there are n-ichoices which we will think of as O(n).
  3. Recurrence: dp[i]=mini+1jnbadness(words[i:j])+dp[j]dp[i] = \min{i+1 \leq j \leq n} \texttt{badness}(\texttt{words[i:j]}) + dp[j]. Time per subproblem is constant x [i+1..n] which is O(n).
  4. Check recurrence is acyclic/topo order: n, n-1, ... 1
  5. Total time: number of subproblems x time per subproblem = O(n^2)
  6. Original problem: dp[0].

§ Parent pointers


Remember which guess was best. Find actual solution, not just the cost of the

§ fiat


fiat lux.

Let there be light solution.
n suffixes | exp many sybsets. dont need to know history!

§ Blackjack


whatever, I don't particularly care about the game

§ DP 3: Paranthesization, Edit distance, knapsack


§ Sequences


Good choices of objects to perform DP on:

§ Parenthesiztion


optimal order of associative expression.: A[0] . A[1] ... A[n-1]. Order matters for matmul!
(A[0] ... A[k-1]) * (A[k] ... A[n-1])`


dp[i][j] = min{
  for k in range(i+1, j):
    dp[i][k] + dp[k][j] + cost of (A[i:k] * A[k:j])
}

§ Edit distance

Given two strngs x and y. Find the cheapest way to convert x into y. We allow character edits to turn x into y: We can (1) insert a character anywhere in x, (2) delete a character anywhere in x, (3) edit any character in x. We have custom costs for each insert and delete.

  1. Replace x[i] with y[j].
  2. Insert y[j].
  3. Delete x[i].

dp[i][j] = min{
  cost of replace x[i] with y[j] + dp[i+1, j+1],
  cost of insert y[j] + dp[i][j+1],
  cost of delete x[i] + dp[i+1][j],
}

The topological order is going to have smaller to larger suffixes.
What I found really interesting is the offhand remark that longest common
substring is just edit distance where we are allowed to only delete
or keep characters.

§ Knapsack


List of items, each of size s[i] and a desire/value v[i]. The sizes are integers. We have a backpack of total size S. We want to choose a subset of the items which maximize the value, and also fit into the backpack: s[i]S\sum s[i] \leq S.

# v WRONG
dp[i] = ... max(dp[i+1], dp[i+1] + v[i])`

We don't keep track of the sizes! Rather, we choose our subproblem to be the suffix AND the remaining capacity xSx \leq S. We have O(nS)O(n S) subproblems
# v correct
dp[i][s] = max(dp[i+1][s], dp[i+1][s-s[i]] + v[i])

To be polynomial in the input, it would have to be θ(nlogS)\theta(n \log S) because SS is given as a number. It would not be nSnS; SS is exponential in the input encoding logS\log S.

§ R21: DP: Knapsack


We have n decisions: d[i] is do I take item i. What do we need to keep track of? I need to know how much weight I have left. This is equivalent to knowing the sum of the items. The edge is an arbitrary item, the weight is -v[i] since we're trying to phrase the problem in terms of shortest path. The state in the node is item I'm looking at, and weight of the items I've taken so far.
\begin{matrix} \end{matrix}

§ DP4: Guitar fingering, tetris, super mario bros


A second kind of guessing. Guessing usually which subproblem to use to solve a bigger subproblem. Another way of guessing is to add more subproblems to guess or remember more features of the solution.

§ Mapping to knapsack


obvious solution was suffix in knapsack. So we needed to know how many units of the knapsack we've used up; we're remembering something about the prefix (but not the full prefix itself). On the other hand, in the forward direction, we were solving more types of subproblems, for varying sizes of knapsacks.

§ Piano and guitar fingering: Take 1


Given some musical piece to play: a sequence of n notes we want to play. We want to find a fingering for each note. We have fingers 1 upto f. We want to assign a finger to each note. We have a difficulty measure d(p, f, p', f'): how hard is to transition from note p (p for pitch) with finger f to note p' with finger f'?

dp[i] = min({
   for f in fingers:
      dp[i+1] + d(i, f, i+1, ?) # WRONG: don't know ?
})


dp[i][f] = min({
  for g in fingers:
      dp[i+1][g] + d[notes[i], f, notes[i+1], g]
}


for i reversed(range(n)):
   for f in range(F): ...


§ guitar chords:


Generalize the notion of "finger" to "finger(F) + string(S)". This gives us O(N((F+S)2))=O(NF2S2)O(N((F+S)^2)) = O(NF^2S^2). Multiple notes: notes[i] = list of F notes for a piano.

§ Tetris


We know the entire sequence of pieces that's going to fall. For each, we must drop the piece from the top. Also, full rows don't clear. The width of the board is small. The board is initially emoty. Can you survive?
The subproblems are how to play suffixes of pieces[i:]. We need to know what the board looks like. If the board doesn't clear and we always drop from the top, then all we need to know is the skyline.


1| ###
2| #
3|####
4|####
5|####



§ Super Mario Bros


Recall that in the original super mario bros, if something moves out of the screen it's lost forever; We can't move back in the old mario. We're given the level, and a small w×hw\times h screen. The configuration of the game is everything on the screen! Total info is going to be cwhc^{wh} where c is some constant. We also need Mario's velocity, the score, and time. score can be S big, time can be T big. The number of configurations is the product of all of these. We also need to know how far to the right we have gone, which is another W. Draw a graph of all configurations, and then use DP

§ Lecture 10: Advanced DP by Srinivas



§ Longest palindromic sequence


Find palindrome inside longer word. Given a string X[1..n]. Find longest palindrome that is a subsequence.
character
c arac

answer will be greater than or equal to 1 in length because a single letter is a palindrome.
turboventilator
  r o   t  ator


def L(i, j): # closed interval
  if i > j: return 0 # no letters
  if i == j: return 1 # single letter palindrome
  if x[i] == x[j]:
    return 2 + l(i+1, j-1)
  return max(L(i+1, j), L(i, j-1))

§ Optimal binary search trees


Find most balanced BST for a set of keys. We have weights for the keys, which are search probabilities. Find a BST T (there are exponential number of BSTs) that minimizes iwi(depthT(ki)+1)\sum_i w_i (depth_T(k_i) + 1). Depth of root is 0. Depth of a node is distance from the root. This minimizes expected search cost.

§ Enumeration


We have exponentially many trees.

§ Greedy soltution / Why doesn't greedy work?


Assume the keys are sorted. Pick K[i] in some greedy fashion (max.weight). This immediately splits the set of keys into the left and right. If we define e(i, j) to be the cost of the optimal BST on keys k[i], ..., k[j] .
greedy:
-------
        2|w=10
1|w=1           4|w=9
           3|w=8

optimal:
-------
         3|w=8
    2|w=10    4|w=9
1|w=1

§ DP



-- e(i, j): cost of tree with keys k: i <= k <= j
def e(i, j):
  if i == j: return w[i]
  # | WRONG
  return min([e(i, r-1) + e(r+1, j) + w[r]
              for k in range(i, j+1)])

The weights are going to change when we increase depth, so we actually need to add all the weights from i to j! So we write:
-- e(i, j): cost of tree with keys k: i <= k <= j
def e(i, j):
  if i == j: return w[i]
  # | WRONG
  return min([e(i, r-1) + e(r+1, j) + weight(i, j)
                 for k in range(i, j+1)])

§ Alternating coins

Have a list of coins. We have an even number of coins. Can only pick coins from the outside.
First player can always not lose in the game. What the first player does is compute v[1] + v[3] + ... v[n-1] versus v[2] + v[4] + ... which is even. If the odd values win, the he picks v[1]. P2 can pick v[2] or v[n]. P1 can pick either v[3] or v[n-1] depending on if P2 picked v[2] or v[n].

v(i, j) = max([ range is(i+1, j) with P2 + v[i],
                range is (i, j-1) with P2 + v[j] ])

If we have v(i+1, j) subproblem with the opponent picking, we are guaranteed that the opponent plays min(v(i+1, j-1), v(i+2, j)). So we can unfold this, to get the full DP:
v(i, j) = max([  min(v(i+1, j-1), v(i+2, j)) + v[i],
                  min(v(i, j-1), v(i+1, j)) + v[j],

§ DP: All pairs shortest paths.