Dynamic Programming

The basic idea of dynamic programming is the opposite of the greedy strategy: one implicitly explores the space of all possible solutions by carefully decomposing things into a series of subproblems, and then building up correct solutions to larger and larger subproblems.

Weighted Interval Scheduling

The Interval Scheduling Problem is the special case in which all weights are equal to 1. Therefore, most greedy algorithms will not solve this problem optimally.

There are nn requests labeled 1,...n1, ... n, with each request ii has a start time sis_i, a finish time fif_i, and a value viv_i. The goal is to select a subset of mutually compatible intervals to maximize the sum of the values of the selected intervals.

Suppose that the requests are sorted in order of the finishing time. p(j)p(j) is defined as the largest index i<ji < j that intervals ii and jj are disjoint.

Let OO be the optimal solution, either interval nn belongs to OO, or it doesn't. If nn belongs to OO, no interval between p(n)p(n) and nn belong to OO.

Let OjO_j denotes the optimal solution of smaller problems of the form 1,2,…,j{1, 2, \dots, j}, and OPT(j)OPT(j) denotes the total weight of the solution. Since jj could either belongs to OO or not, OPT(j)=max(OPT(p(j))+vj,OPT(j−1))OPT(j) = max(OPT(p(j)) + v_j, OPT(j - 1)).

def compute_opt(j):
  if j == 0:
    return 0
  return max(
    values[j] + compute_opt(p(j)),
    compute_opt(j - 1)
  )

However, the recursion would take exponential time to run in the worst case, since the recursion tree widens quickly due to the recursive branching.

Memoizing the Recursion

In fact, the recursive algorithm only solves n+1n + 1 different subproblems. To eliminate the redundancy, the result of each subproblem could be cached, which is called memoization.

cache = {
  0: 0
}

def mem_compute_opt(j):
  if j in cache:
    return cache[j]

  cache[j] = max(
    values[j] + compute_opt(p(j)),
    compute_opt(j - 1)
  )
  return cache[j]

The progress measure is the number of keys in cache. Initially the number is 1, and it will grow to n+1n + 1, since there are total n + 1 unique subproblems. The running time of mem_compute_opt is O(n)O(n) if the intervals are sorted by their finishing time.

Find Solution

def find_solution(j):
  if j == 0:
    return []
  if value[j] + cache[p(j)] > cache[j - 1]:
    return find_solution(p(j)) + [j]
  return find_solution(j - 1)

Since the recursive calls only on strictly smaller values, it makes a total of O(n)O(n) recursive calls. Therefore, the function returns an optimal solution of the problem in O(n)O(n).

Principle of Dynamic Programming

Designing the Algorithm

def iterative_compute_opt:
  cache[0] = 0
  for j in range(1, n + 1):
    cache[j] = max(
      value[j] + cache[p(j)],
      cache[j - 1]
    )

The running time of the iterative algorithm is O(n)O(n), since each iteration takes constant time.

Basic Outline of Dynamic Programming

To develop an algorithm based on dynamic programming, the collection of subproblems derived from the original problem has to satisfies a few basic properties.

  1. The number is subproblems is polynomial.

  2. The solution can be easily computed from the solutions to the subproblems.

  3. There's a natural ordering on subproblems from "smallest" to "largest" with a neasy-to-compute recurrence that could determine the solution of a subproblem from smaller subproblems.

RNA Secondary Structure

Double-stranded DNA is zipped together by complementary base-pairing. Each base is drawn from the set A,C,G,T{A, C, G, T}, in which A−TA-T and C−GC-G forms the pairings.

Single-stranded RNA loops back and form base pairs with itself, which is the secondary structure of RNA. A single-stranded RNA molecule can be viewed as a sequence of nn symbols drawn from the set A,C,G,U{ A, C, G, U }.

The secondary structure on B=b1,b2,…,bnB = b_1, b_2, \dots, b_n is a set of pairs S=(i,j)S = {(i, j)}, where i,j∈1,2,…,ni, j \in { 1, 2, \dots, n }. The secondary structure is formed with the optimum total free energy, or maximum number of base pairs.

  • SS is a matching.

  • If (i,j)∈S(i, j) \in S, then i<j−4i < j - 4.

  • The elements of any pair consist of either A,U{ A, U } or C,G{ C, G }.

  • If (i,j)(i, j) and (k,l)(k, l) are two pairs in SS, then we can't have i<k<j<li < k < j < l.

First Attempt at Dynamic Programming

Let OPT(j)OPT(j) be the maximum number of base pairs in a structure on b1,b2,…,bjb_1, b_2, \dots, b_j. OPT(j)=0OPT(j) = 0 for j≤5j \leq 5.

  • jj is not involved in a pair: OPT(j)=OPT(j−1)OPT(j) = OPT(j - 1)

  • jj pairs with tt for some t<j−4t< j - 4: OPT(j)=OPT(t−1)+OPT(j) = OPT(t - 1) + the optimal solution between t+1t + 1 and j−1j - 1.

Since there's no way to represent the optimal solution between t+1t + 1 and j−1j - 1, a seperate variable is introduced.

Dynamic Programming over Intervals

Let OPT(i,j)OPT(i, j) denote the maximum number of base pairs in a structure on bi,bi+1,…,bjb_i, b_{i + 1}, \dots, b_j. OPT(i,j)=0OPT(i, j) = 0 for i≥j−4i \geq j - 4.

  • jj is not involved in a pair: OPT(i,j)=OPT(i,j−1)OPT(i, j) = OPT(i, j - 1)

  • jj pairs with tt for some t<j−4t < j - 4: OPT(i,j)=OPT(i,t−1)+OPT(t+1,j−1)+1OPT(i, j) = OPT(i, t - 1) + OPT(t + 1, j - 1) + 1.

Therefore, OPT(i,j)=max(OPT(i,j−1),max(OPT(i,t−1)+OPT(t+1,j−1)+1))OPT(i, j) = max(OPT(i, j - 1), max(OPT(i, t - 1) + OPT(t + 1, j - 1) + 1)) for each tt such that btb_t and bjb_j are allowable base pair.

There are O(n2)O(n ^ 2) subproblems to solve, and evaluating the recurrence takes time O(n)O(n). Therefore, the total running time is O(n3)O(n ^ 3).

Sequence Alignment

Let XX and YY be two strings, where XX consists of the sequence of symbols x1,x2,…,xmx_1, x_2, \dots, x_m and YY consists of the sequence of symbols y1,y2,…,yny_1, y_2, \dots, y_n. A matching MM of these two sets is an alignment if there are no crossing pairs. If (i,j),(i′,j′)∈M(i, j), (i', j') \in M and i<i′i < i', then j<j′j < j'.

The definition of similarity is based on finding the optimal alignment between the two strings. The process of minimizing the costs is referred to as sequence alignment.

  • There's a parameter $ \epsilon > 0 $ that defines a gap penalty. For each position that is not matched (a gap), it incurs a cost of ϵ\epsilon.

  • For each pair of letters p,qp, q, there's a mismatch cost of apqa_{pq} for lining up pp with qq. For each (i,j)∈M(i, j) \in M, it incurs the cost axi,yjforliningupa_{x_i, y_j} for lining up x_i withwith y_j .. a_pp = 0foreachletterfor each letter p $$.

  • The cost of MM or the similarity between XX and YY is the sum of its gap and mismatch costs.

Designing the Algorithm

In the optimal alignment MM, either (m,n)∈M(m, n) \in M or (m,n)∉M(m, n) \notin M. (m,nm, n are the last symbols in the two strings.)

Let MM be any alignment of two strings. If (m,n)∉M(m, n) \notin M, then either the mthm^{th} position of XX or the nthn^{th} position of YY is not matched in MM.

Therefore, in an optimal alignment, at least one of the following is true:

  • (m,n)∈M(m, n) \in M

  • The mthm^{th} position of XX is not matched

  • The nthn^{th} position of YY is not matched

Let OPT(i,j)OPT(i, j) denote the minimum cost of an alignment between two strings.

  • Case 1: Pay axm,yna_{x_m, y_n}, and OPT(m,n)=axm,yn+OPT(m−1,n−1)OPT(m, n) = a_{x_m, y_n} + OPT(m - 1, n - 1)

  • Case 2: Pay ϵ\epsilon, OPT(m,n)=ϵ+OPT(m=1,n)OPT(m, n) = \epsilon + OPT(m = 1, n)

  • Case 3: Pay ϵ\epsilon, OPT(m,n)=ϵ+OPT(m,n−1)OPT(m, n) = \epsilon + OPT(m, n - 1)

Therefore, the minimum alignment costs satisfy the recurrence: OPT(i,j)=min(axm,yn+OPT(m−1,n−1),ϵ+OPT(m−1,n),ϵ+OPT(m,n−1))OPT(i, j) = min(a_{x_m, y_n} + OPT(m - 1, n - 1), \epsilon + OPT(m - 1, n), \epsilon + OPT(m, n - 1)).

Analyzing the Algorithm

The running time is O(mn)O(mn), since there are mnmn subproblems, and it takes constant time to solve each. The algorithm takes O(mn)O(mn) space, which is the size of the array.

Sequence Alignment in Linear Space

The dynamic programming algorithm to the sequence alignment problem takes O(mn)O(mn) space, which is not suitable for computation of large strings. With a nice application of divide-and-conquer idea, the space complexity could be reduced to O(mn)O(mn).

Designing the Algorithm

If we only care about the value of the optimal alignment, it's easy to reduce the array to 2×n2 \times n, since computing the result of each subproblem only requires the result in the current and previous row. Therefore, it costs O(mn)O(mn) time and O(n)O(n) space.

To construct the alignment, we could use a backward formulation of the dynamic programming algorithm. Let g(i,j)g(i, j) denotes the length of the shortest path from (i,j)(i, j) to (m,n)(m, n), and starts with g(m,n)=0g(m, n) = 0.

For i<mi < m and j<nj < n, we have g(i,j)=min(ax+1,y+1+g(i+1,j+1),ϵ+g(i,j+1),ϵ+g(i+1,j))g(i, j) = min(a_{x+1, y+1} + g(i + 1, j + 1), \epsilon + g(i, j + 1), \epsilon + g(i + 1, j)). The space complexity backward formulation could also be reduced to O(n)O(n) by using an array of 2×n2 \times n.

The length of the shortest corner-to-corner path in GxyG_{xy} that passes through (i,j)(i, j) is f(i,j)+g(i,j)f(i, j) + g(i, j).

Let kk be any number in 0,…,n{0, \dots, n}, and qq be an index that minimizes the quantity f(q,k)+g(q,k)f(q, k) + g(q, k). There's a corner-to-corner path of minimum length that passes through (q,k)(q, k).

The algorithm divides GXYG_{XY} along its center column and compute the value of f(i,n/2)f(i, n / 2) and g(i,n/2)g(i, n / 2) for each value of ii. Then the algorithm could determine the minimum value of f(i,n/2)+g(i,n/2)f(i, n / 2) + g(i, n / 2). Therefore, there's a shortest corner-to-corner path passing the node (i,n/2)(i, n / 2). Thus, the algorithm could search again between (0,0)(0, 0) and (i, n / 2) andbetweenand between (i, n / 2) andand (m, n) $$.

Analyzing the Algorithm

The algorithm maintains an array with at most m+nm + n entires, which holds nodes on the shortest corner-to-corner path as they are discovered. Therefore, the space complexity is O(m+n)O(m + n).

Let T(m,n)T(m, n) denote the maximum running time of the algorithm on strings of length mm and nn.

  • T(m,n)≤cmn+T(q,n/2)+T(m−q,n/2)T(m, n) \leq cmn + T(q, n / 2) + T(m - q, n / 2)

  • T(m,2)≤cmT(m, 2) \leq cm

  • T(2,n)≤cnT(2, n) \leq cn

Suppose that m=nm = n and pp is exactly in the middle, we could write T()˙T(\dot) in terms of the single variable nn, and let q=n/2q = n / 2. Therefore, T(n)≤2T(n/2)+cn2T(n) \leq 2T(n / 2) + cn^2, which implies T(n)=O(n2)T(n) = O(n ^ 2). Therefore, the time complexity is O(mn)O(mn).

Shortest Paths in a Graph

Let G=(V,E)G = (V, E) be a directed graph, and each edge (i,j)∈E(i, j) \in E has a weight cijc_{ij}. Dijkstra's Algorithm could only find shortest paths with non-negative weights.

Negative cycles is a directed cycle CC such that ∑ij∈Ccij<0\sum_{ij \in C} c_ij < 0. If the graph has no negative cycles, find a path from ss to tt with minimum total cost ∑ij inPcij\sum_{ij \ in P} c_ij.

The Bellman-Ford Algorithm

If GG has no negative cycles, then there's a shortest path from ss to tt that is simple, and hense has at most n−1n - 1 edges.

Let OPT(i,v)OPT(i, v) be the minimum cost of a v−tv-t path with at most ii edges. The original problem is OPT(n−1,s)OPT(n - 1, s).

  • If a path uses at most i−1i - 1 edges, then OPT(i,v)=OPT(i−1,v)OPT(i, v) = OPT(i - 1, v).

  • If a path uses ii edges, and the first edge is (v,w)(v, w), then OPT(i,v)=cvw+OPT(i−1,w)OPT(i, v) = c_{vw} + OPT(i - 1, w).

Therefore, the recursive formula is OPT(i,v)=min(OPT(i−1,v),minw∈V(OPT(i−1,w+cvw)))OPT(i, v) = min(OPT(i - 1, v), min_{w \in V} (OPT(i - 1, w + c_{vw}))).

Analyzing the Algorithm

Let GG be a graph with mm edges and nn nodes. Let nvn_v be the number of nodes ww which $$ v $ has an edge to ww, thus it takes O(nv)O(n_v) to compute OPT(i,v)OPT(i, v). Since 0≤i≤n−10 \leq i \leq n - 1, the total running time is O(n∑v∈Vnv)O(n \sum_{v \in V} n_v). Since ∑v∈Vnv=m\sum_{v \in V} n_v = m in a directed graph, the total running time is O(mn)O(mn).

Improving the Space Complexity

Rather than recording OPT(i,v)OPT(i, v) for each value ii, the algorithm will use and update a single value OPT(v)OPT(v) for each node vv, the length of the shortest path from vv to tt that the algorithm have found so far. Therefore, the algorithm still runs for i=1,2,…,n−1i = 1, 2, \dots, n - 1, and OPT(v)=min(OPT(v),minw∈V(cvw+OPT(w)))OPT(v) = min(OPT(v), min_{w \in V} (c_{vw} + OPT(w))).

To recover the path, the algorithm maintains a map firstfirst, in which first[v]first[v] is the first node (after itself) on the shortest path from vv to tt.

Let PP be the dirrected pointer graph whose nodes are VV, and edges are (v,first[v]){(v, first[v])}. If the pointer graph contains a cycle, then this cycle must have negative cost. Since GG doesn't have negative cycles, PP will never have a cycle. Therefore, the shortest path from vv to tt is first[v],first[first[v]],…,tfirst[v], first[first[v]], \dots, t.

Last updated