Dynamic Programming Basics

This page mainly introduces the basic ideas of dynamic programming, as well as approaches to designing states and state transition equations in dynamic programming, helping beginners gain a preliminary understanding.

Other pages in this section introduce methods for building dynamic-programming models for various types of problems, as well as various optimization techniques for dynamic programming.

Introduction

[IOI1994] Digital Triangle

Given a number triangle with rr rows (r≤1000r \leq 1000), find a path that starts at the top and ends at any position on the bottom, maximizing the sum of the numbers along the path. At each step, one may move to the point immediately down-left or down-right of the current point.

        7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

In the example above, the optimal path is 7→3→8→7→57 \to 3 \to 8 \to 7 \to 5.

The simplest brute-force idea is to try all paths. Since the number of paths is on the order of O(2r)O(2^r), this approach is unacceptable.

Notice the following fact: every decision made along an optimal path is itself optimal.

For the optimal path mentioned in the example, consider only the first four steps, 7→3→8→77 \to 3 \to 8 \to 7. There is no path from the top to the 22-nd number in the 44-th row with a greater weight.

For each point, there are only two choices for the next step: move down-left or down-right (if it exists). Therefore, we only need to record the maximum weight at the current point and use this maximum weight to make the next decision, updating the maximum weights of subsequent points.

This has another benefit: we successfully reduce the problem size by dividing one problem into multiple smaller problems. To obtain the optimal solution from the top to the rr-th row, we only need the information about the optimal solution from the top to the r−1r-1-th row.

There is still a problem: the overlapping parts among subproblems are numerous, and the same subproblem may be visited repeatedly, so the efficiency remains low. The solution is to store the solution to each subproblem and use memoization to restrict the visitation order, ensuring that each subproblem is visited only once.

These are some basic ideas of dynamic programming. The following sections will introduce the ideas of dynamic programming more systematically.

Principles of dynamic programming

Problems solvable by dynamic programming need to satisfy three conditions: optimal substructure, no aftereffect, and overlapping subproblems.

Optimal substructure

Having optimal substructure may also indicate that a problem is suitable for a greedy solution.

Make sure that all subproblems used in the optimal solution are considered.

  1. Prove that the first component of an optimal solution to the problem is making a choice;
  2. For a given problem, among its possible first-step choices, assume that you already know which choice will produce an optimal solution. You do not care how this choice was obtained; simply assume that it is already known;
  3. Given the choice that yields an optimal solution, determine which subproblems this choice produces and how to best characterize the subproblem space;
  4. Prove that, as components of the optimal solution to the original problem, the solution to each subproblem is itself optimal. Use proof by contradiction: suppose the solution added for a subproblem is not its own optimal solution. Then replace the current non-optimal solution in the original problem with the subproblem's optimal solution to obtain a better solution to the original problem, contradicting the assumption that the original solution is optimal.

Keep the subproblem space as simple as possible and expand it only when necessary.

The differences in optimal substructure appear in two aspects:

  1. How many subproblems are involved in the optimal solution to the original problem;
  2. How many choices must be considered when determining which subproblems are used by the optimal solution.

Each vertex in a subproblem graph corresponds to a subproblem, while the choices that need to be considered correspond to edges incident to the subproblem vertices.

No aftereffect

A subproblem that has already been solved will not be affected by subsequent decisions.

Overlapping subproblems

If there are many overlapping subproblems, we can use space to store their solutions, avoiding repeated solutions of the same subproblems and thereby improving efficiency.

Basic idea

For a problem solvable by dynamic programming, the following approach is generally used:

  1. Divide the original problem into several stages, with each stage corresponding to several subproblems, and extract the features of these subproblems (called states);
  2. Find the possible decisions for each state, or the transitions among states (described mathematically as the state transition equations);
  3. Solve the problems in each stage in order.

From the perspective of graph theory, construct a directed acyclic graph, where each state corresponds to a node in the graph and decisions correspond to edges between nodes. The problem is thereby transformed into finding a longest (shortest) path on a DAG (see DP on a DAG).

Longest common subsequence

Longest common subsequence problem

Given a sequence AA of length nn and a sequence BB of length mm (n,m≤5000n,m \leq 5000), find a longest sequence that is a subsequence of both AA and BB.

The definition of a subsequence can be found in Subsequence. A brief example: the common subsequences of the strings abcde and acde include a, c, d, e, ac, ad, ae, cd, ce, de, acd, ade, ace, cde, and acde; the length of the longest common subsequence is 4.

Let f(i,j)f(i,j) denote the length of the longest common subsequence when considering only the first ii elements of AA and the first jj elements of BB. Finding the length of the longest common subsequence in this situation is the subproblem. f(i,j)f(i,j) is the state described above, and f(n,m)f(n,m) is the final state to be reached, namely the desired result.

For each f(i,j)f(i,j), there are three decisions. If Ai=BjA_i=B_j, it can be appended to the end of the common subsequence; the other two decisions are to skip AiA_i or BjB_j. The state transition equation is:

f(i,j)={f(i−1,j−1)+1Ai=Bjmax⁡(f(i−1,j),f(i,j−1))Ai≠Bjf(i,j)=\begin{cases}f(i-1,j-1)+1&A_i=B_j\\\max(f(i-1,j),f(i,j-1))&A_i\ne B_j\end{cases}

See SourceForge's interactive LCS page for a better understanding of the LCS implementation process.

Reference implementation

C++

int n, m, a[MAXN], b[MAXM], f[MAXN][MAXM];

int dp() {
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
      if (a[i] == b[j])
        f[i][j] = f[i - 1][j - 1] + 1;
      else
        f[i][j] = std::max(f[i - 1][j], f[i][j - 1]);
  return f[n][m];
}

Python

def dp(n, m, a, b):
    f = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if a[i] == b[j]:
                f[i][j] = f[i - 1][j - 1] + 1
            else:
                f[i][j] = max(f[i - 1][j], f[i][j - 1])
    return f[n][m]

This method has time complexity O(nm)O(nm).

In addition, this problem has an O(nmw)O\left(\dfrac{nm}{w}\right) algorithm1. Interested students can explore it on their own.

Longest non-decreasing subsequence

Longest non-decreasing subsequence problem

Given a sequence aa of length nn (n≤5000n \leq 5000), find a longest subsequence of aa such that each element is no smaller than the preceding element.

Algorithm 1

Let f(i)f(i) denote the length of the longest non-decreasing subsequence ending at aia_i. The desired answer is max⁡1≤i≤nf(i)\max_{1 \leq i \leq n} f(i).

When calculating f(i)f(i), try appending aia_i to the ends of other longest non-decreasing subsequences to update the answer. Thus, the following state transition equation can be written: f(i)=max⁡1≤j<i, aj≤ai(f(j)+1)f(i)=\max_{1 \leq j < i,~a_j \leq a_i} (f(j)+1).

Reference implementation

C++

int n, a[MAXN], d[MAXN];

int dp() {
  d[1] = 1;
  int ans = 1;
  for (int i = 2; i <= n; i++) {
    d[i] = 1;
    for (int j = 1; j < i; j++)
      if (a[j] <= a[i]) {
        d[i] = std::max(d[i], d[j] + 1);
        ans = std::max(ans, d[i]);
      }
  }
  return ans;
}

Python

def dp(n, a):
    d = [0] * (n + 1)
    d[1] = 1
    ans = 1
    for i in range(2, n + 1):
        d[i] = 1
        for j in range(1, i):
            if a[j] <= a[i]:
                d[i] = max(d[i], d[j] + 1)
                ans = max(ans, d[i])
    return ans

It is easy to see that this algorithm has time complexity O(n2)O(n^2).

Algorithm 2

When the range of nn expands to n≤105n \leq 10^5, the first approach is no longer fast enough. The following gives an O(nlog⁡n)O(n \log n) approach.

Consider the previously defined state (i,l)(i, l), representing that the longest non-decreasing subsequence ending at the ii-th element has length ll. Unlike the previous method, which processes states by fixed ii, here we directly determine whether (i,l)(i, l) is valid:

  • The initial state (1,1)(1,1) is necessarily valid.
  • For any (i,l)(i, l), if there exists a j<ij < i such that (j,l−1)(j, l-1) is valid and aj≤aia_j \le a_i, then (i,l)(i, l) is valid.

Finally, we only need to find the valid state (i,l)(i,l) with the largest ll to obtain the length of the longest non-decreasing subsequence.

Let the original sequence be a1,⋯ ,ana_1, \cdots, a_n, and define an array dd, where the xx-th position stores the minimum possible final element of a non-decreasing subsequence of length xx. Initially the sequence is empty. Let ii iterate from 11 to nn, and successively find the length of the longest non-decreasing subsequence among the first ii elements. For the current element aia_i:

  • If aia_i is greater than or equal to the last element in sequence dd, directly insert aia_i at the end of sequence dd.
    • Explanation: if aia_i is greater than or equal to the final element of the current longest subsequence, a non-decreasing subsequence exists that can append aia_i. Not inserting it would destroy optimality.
  • If aia_i is strictly smaller than the last element in dd, find the first element greater than it and replace that element with aia_i.
    • Explanation: inserting it at the end would destroy the monotonicity of dd; replacement ensures that the final element for each length is as small as possible, preserving more possibilities for subsequent elements.
    • Optimization: because dd is non-decreasing, binary search can directly find the insertion position, reducing the overall complexity to O(nlog⁡n)O(n\log n) instead of the O(n2)O(n^2) complexity of brute-force searching.

If the actual longest non-decreasing subsequence must also be output, additionally maintain an array dx′d'_x, representing the position of the minimum final element among non-decreasing subsequences of length xx (if there are several, any one may be chosen). During maintenance, when inserting element aia_i into dxd_x, simultaneously update dx′d'_x to ii. Also record the optimal predecessor pip_i of ii as dx−1′d'_{x-1}. Finally, starting from any valid state of maximum length and tracing backward along predecessors pip_i, the complete subsequence can be obtained.

Reference implementation

C++

int n, a[MAXN], d[MAXN], di[MAXN], pre[MAXN], res[MAXN];

int dp() {
  int ans = 0;
  for (int i = 1; i <= n; ++i) {
    int tmp = std::upper_bound(d, d + ans, a[i]) - d;
    pre[i] = tmp ? di[tmp - 1] : -1;
    d[tmp] = a[i];
    di[tmp] = i;
    if (tmp == ans) ++ans;
  }
  // Construct the subsequence.
  for (int k = ans, i = di[ans - 1]; k; --k) {
    res[k] = a[i];
    i = pre[i];
  }
  return ans;
}

Python

n = 0
a = [0] * MAXN
d = [0] * MAXN
di = [0] * MAXN
pre = [0] * MAXN
res = [0] * MAXN


def dp():
    ans = 0
    for i in range(1, n + 1):
        tmp = bisect.bisect_right(d, a[i], 0, ans)
        pre[i] = di[tmp - 1] if tmp else -1
        d[tmp] = a[i]
        di[tmp] = i
        if tmp == ans:
            ans += 1

    # Construct the subsequence
    k = ans
    i = di[ans - 1]
    while k:
        res[k] = a[i]
        i = pre[i]
        k -= 1

    return ans

This algorithm has time complexity O(nlog⁡n)O(n\log n). The time complexity of outputting the answer is O(ans)O(\textit{ans}).

Note

For the strictly increasing subsequence problem, similarly let did_i denote the minimum final element among all longest strictly increasing subsequences of length ii.

Note that in step 2, if ai≤dlena_i \leq d_{len}, because adjacent elements in a longest strictly increasing subsequence cannot be equal, the first element not smaller than aia_i must be found in sequence dd and replaced with aia_i.

In the implementation (taking C++ as an example), change the upper_bound function to lower_bound.

References and notes

Footnotes

  1. Finding the longest common subsequence with bit operations - -Wallace- - Cnblogs ↩

This content comes from OI Wiki: Dynamic Programming Basics. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.