Reference:
- 算法-动态规划 Dynamic Programming–从菜鸟到老鸟
- 五大常用算法之二:动态规划算法
- Huahua Dynamic Programming (videos)
- Elements of Programming Interviews in Java, Dynamic Programming
What is DP?
Those who cannot remember the past are condemned to repeat it. — Dynamic Programing
The important fact to observe is that we have attempted to solve a maximization problem involving a particular value of $x$ and a particular value of $N$ by first solving the general problem involving an arbitrary value of $x$ and an arbitrary value of $N$. — R.E. Bellman, 1957
DP is a programming method
.
Three Requirements:
- Optimal Substructure(最优子结构)
- Can be solved optimally by decomposing it into subproblems adn then recursively find the optimal solutions to the subproblems.
- You should consider using DP whenever you have to make choices to arrive at the solution, specifically, when the solution relates to subproblems.
- Overlapping Subproblems(重叠子问题)
- Subproblems are overlapped such that we can compute only once and store for future use.
- If subproblems do not overlap ->
Divide and Conquer
(they both combine the solutions of multiple smaller problems)
- No-after Effect(无后效性)
- The optimal solution of a sub-problem will not change when it was used to solve a bigger problem optimally.
Applied to Two Kinds of Problems:
Optimization(优化问题)
或Counting(计数问题)
(min、max)
For some problems, DP will not work, e.g. the longest path problem.
Key Steps & Problem Types
Key Steps
- Find the recurrence(Big problem -> subproblems)
- The most difficult part.
- Simple recursive solution incurs high complexity.
- Has so many overlapped subproblems.
- Optimization (avoid repeated computation)
Top-down
(recursion withmemoization
)- Time complexity is clear / 时间复杂度比较清晰
- Space is usually cannot be optimized / 一般进行不了空间优化
Bottom-up
- Space can be readily optimized.
- Sometimes, recursion may out-perform a bottom-up DP solution, e.g., when the solution is found early or subproblems can be pruned through bounding.
Problem Types
50%
: Sequence / 序列型- LIS / 单序列
- LCS / 双序列(common)
30%
: Pascal Triangle / 矩阵坐标型15%
: Knapsack Variants (coin change, subset sum)5%
: Interval or others / 区间型及其他(optimal BST, matrix product, generate binary trees)- A bit difficult
Fibonacci Number (1D)
Recurrence: $F(n) = F(n - 1) + F(n - 2)$, where $F(0) = 0$ and $F(1) = 1$.
A function to compute $F(n)$ that recursively invokes itself has a time complexity that is exponential in $n$. This is because the recursive function computes some $F(i)$s repeatedly as shown in the figure below.
1 | // Recursion O(2^n) |
Therefore, we can cache intermediate results and make the time complexity for computing the n-th Fibonacci number linear in $n$, but it is at the expense of $O(n)$ storage.
1 | // Top-down O(n) |
Further, minimizing cache space is a recurring theme in DP. In a bottom-up fashion, we can reduce the space complexity of the cache.
1 | // Bottom-up O(n) it still needs O(n) space |
Related LC Problems:
Unique Paths (2D)
How many possible unique paths are there?
Explanation: 62. Unique Paths (include obstacle version)
Related LC Problems:
- Amazon OA: 1102. Path With Maximum Minimum Value
Other Problems
Multiple States: dp[i][0]
, dp[i][1]
, dp[i][2]
Increasing Sum / Max / Prefix, Suffix:
Palindromic Problems:
The following contents are not organized!
The following contents are not organized!
The following contents are not organized!
Knapsack
Knapsack 是 NP-Complete 问题。
Note:没有任何贪心算法不能给最优解,比如拿 $\frac{v_i}{w_i}$
No Repetition
Input:
v[i]
(i=0..n-1): Valuew[i]
(i=0..n-1): WeightC
: Max Weight
Note: Only one copy of each item at most.
Output: Max value sum.
Type: Optimization
问题:
- 怎么才能递归式地想问题呢?
- 怎么大问题化小问题?
- 怎么才是小问题?
**Note:**这里的 i 表示的是第几个,上面的是索引。
f(i, j)
: 如果只有 1 ~ i
个 item 可以选,并且此时的包重量为 j
,你最多能拿走价值多少钱东西?
子问题:f(i-2, j)
、f(i, j-10)
、f(i-5, j-15)
选择:
- take it:
f(i-1, j-w[i]) + v[i]
- leave it:
f(i-1, j)
- 递归式:
f(i, j)
= max(f(i-1, j-w[i]) + v[i]
,f(i-1, j)
)
Recursion:
Note: 以下访问数组 w 和 v 时 i 都要减去 1。
1 | // O(2^n) |
Top-down:
1 | // dp lookup |
Bottom-up:
f(i, j)
= max(f(i - 1, j)
, f(i - 1, j - w[i - 1]) + v[i - 1]
)
1 | // O(Cn) |
Repetition
Input:
v[i]
(i=0..n-1): Valuew[i]
(i=0..n-1): WeightC
: Max Weight
Note: Infinite
copies of each item at most.
Output: Max value sum.
Type: Optimization
关键点:拿东西后这个东西还在,只是包的可用重量变小了。
Idea: f(i)
是当包的大小为i
时,最多能拿走价值多少钱的东西。
递归式:f(i)
= $\max_{k=1\ldots n}$(f(i - w[k])
+ v[k]
)
Recursion:
1 | int ks(int[] v, int[] w, i) { |
Top-down:
1 | int[] dp = new int[](n + 1); // and init with -1 |
Bottom-up:
递归式:f(i)
= $\max_{k=1\ldots n}$(f(i - w[k])
+ v[k]
)
1 | int ks_bu(int[] v, int[] w, int C) { |
Variants
有一个国家发现了 5 座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是 10 人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
332. Coin Change
Bottom-up:
1 | public int coinChange(int[] coins, int amount) { |
你好,我是分界线
Two methods:
- Top-down (Memo) => Extra memory
- Bottom-up => Better
Key steps:
- Recursion
- Store (Memoize intermediate results)
- Bottom-up (optional)
钢条切割
对于 $r_n(n \geq 1)$,我们可以用更短的钢条的最优切割收益来描述:
$$r_n = \max(p_n, r_1 + r_{n-1}, r_2 + r_{n-2}, \ldots, r_{n-1} + r_1)$$
For example, when $n = 3$ we have as follows:
$$r_3 = \max(p_3, r_1 + r_2, r_2 + r_1)$$
第一个参数 $p_n$ 对应不切割,直接求出长度为 $n$ 的方案,其他 $n-1$ 个参数对应了 $n-1$ 种切割方案。
关于子问题的最优解,并在所有可能的两端切割方案重选取组合收益最大者,构成原问题的最优解。我们称钢条切割问题满足最优子结构
(optimal substructure)性质:
- 问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
钢条切割问题还存在一种相似的但更为简单但递归求解
方法:我们将钢条从左边切割下长度为 $i$ 的一段,只对右边剩下的长度为 $n-i$ 的一段继续进行切割(递归求解),对左边一段不再进行切割。因此,上面的公司可以简化为:
$$r_n = \max_{1 \leq i \leq n}(p_i + r_{n-i})$$
For example, when $n = 3$ we have as follows:
$$r_3 = \max_{1 \leq i \leq 3}(p_1 + r_2, p_2 + r_1, p_3 + r_0)$$
1 | 0 1 2 3 4 5 6 7 8 9 |
Recursive version:
1 | public static int cut(int[] p, int n) { // n is the length |
Memo version:
1 | public static int cutMemo(int[] p) { |
这道钢条切割问题的经典之处在于自底向上的动态规划问题的处理,理解了这个也就理解了动态规划的精髓。
Bottom-up dp version:
1 | public static int bottom_up_cut(int[] p) { |
0-1 Knapsack
Naive Recursive Solution:
1 | int[] v; |