总结一下与DP相关的问题。
64, 300(LIS), LCS, 120, 62, 303, 198,
DP特点:
- 把重复计算的结果保存下来,避免重复计算(记忆机制)。
- DP自下而上执行,通过可计算的最小子结构逐步向上求值,直到得到最终值。
- 通常问题是要求最优值。
实现DP方法:
- 画递归树,自顶向下思考,自下而上实现
- 找到合适的状态及状态转移方程
- 状态:递归树的结点啥定义,即每一步记忆的内容是啥
memo[i]
,即函数是啥。 - 状态转移方程:结点怎样实现,即怎样得到
memo[i]
,即函数怎么做。
- 状态:递归树的结点啥定义,即每一步记忆的内容是啥
#64 Minimum Sum Path
问题描述:
1 | Given a m x n grid filled with non-negative numbers, find a path from **top left** to **bottom right** which minimizes the sum of all numbers along its path. |
思路
记忆的逐步实现:
初始化:memo所有元素初始化为0,且第一行第一列特殊处理。
状态定义:
memo[i][j]
表示从左上角到Input[i][j]
的最短路径。状态转移方程:
memo[i][j] = Input[i][j] + min(memo[i][j-1], memo[i-1][j])
。返回值:
memo[i-1][j-1]
图示:
最终返回memo右下角值:7。
看图说话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
for (int i=1; i<m; i++)
grid[i][0] += grid[i-1][0];
for (int i=1; i<n; i++)
grid[0][i] += grid[0][i-1];
for (int i=1; i<m; i++){
for (int j=1; j<n; j++)
grid[i][j] += min(grid[i][j-1], grid[i-1][j]);
}
return grid[m-1][n-1];
}
#300 Longest Increasing Subsequence
- 描述
1 | Input: [10,9,2,5,3,7,101,18] |
逻辑
- 初始化:mem数组为全1。
- 状态的定义:定义
mem[i]
为以nums[i]
为结尾的最长上升子序列的长度。 - 状态转移方程:当前元素
nums[i]
与其前元素逐个比较,如果当前元素大于其前某个元素nums[j]
,更新mem[i]=max(mem[i], 1+mem[j])
. - 最终:找到
mem
数组中最大的值即可。DONE
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int lengthOfLIS(vector<int>& nums) {
if (nums.size() == 0)
return 0;
// calculate memo
vector<int> memo(nums.size(), 1);
for (int i=1; i<nums.size(); i++){
for (int j=0; j<i; j++){
if (nums[i]>nums[j])
memo[i] = max(memo[i], 1+memo[j]);
}
}
// get max length
int res = memo[0];
for (int i=0; i<memo.size(); i++)
res = max(res, memo[i]);
return res;
}
关键:由最小已知求未知,接着根据当前的已知求进一步的未知, 状态的定义,找到状态转移方程,
#LCS Longest Common Subsequence
- 描述
1 | 给定两个字符数组,求其公共的子序列。公共子序列的元素在原序列中可以不连续。 |
- 逻辑
- 实现
1 | string getLCS(const string& s1, const string& s2){ |
#120 Triangle
- 描述
1 | Given a triangle, find the minimum path sum from top to bottom. |
逻辑
bottom-up DP或者Top-down DP。使用Top-down:
状态定义:
triangle[i][j]
表示从顶端到第i
行第j
个位置的最小路径状态转移方程:见下图
Top-down DP过程如下图表示:其中椭圆结点构成的triangle为源triangle,矩形结点构成的triangle就是DP的
mem
,即状态。
看图说话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size(); // height of this triangle
triangle[0][0] = triangle[0][0];
for (int i = 1; i<n; i++){ //
// 这一行第一个值
triangle[i][0] += triangle[i-1][0];
// 这一行最后一个值
triangle[i][i] += triangle[i-1][i-1];
// 这一行其他值
for (int j=1;j<i;j++)
triangle[i][j] += min(triangle[i-1][j-1], triangle[i-1][j]);
}
// 源triangle被修改了,所以最终结果在这个triangle的最后一层找最小值即可
return *min_element(triangle[n-1].begin(), triangle[n-1].end());
}
关键:当处理最值相关问题,考虑DP
#62 Unique Path
- 描述
1 | 从grid的左上角位置到右下角位置有几种走法?只能向下走或向右走。 |
逻辑
Top-down DP或者Bottom-up DP,使用Bottom-up:
- 状态定义:从
(i,j)
出发到终点(m-1,n-1)
由多少条路径。 - 状态转移方程:同Fibonacci(),
numPath(i,j) = numPath(i,j+1) + numPath(i+1,j)
; - 初始化最右边和最下边值为1. 过程从这里开始
up
。
- 状态定义:从
实现
- Bottom-up
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int uniquePaths(int m, int n) {
vector<vector<int>> memo(m, vector<int>(n, -1));
// 初始化最下行
for (int i=0;i<m; i++)
memo[i][n-1] = 1;
// 初始化最右行
for (int j=0; j<n; j++)
memo[m-1][j] = 1;
// Bottom up
for(int i = m-2; i>=0; i-- )
for(int j = n-2; j>=0; j-- )
memo[i][j] = memo[i][j+1] + memo[i+1][j];
return memo[0][0];
} - Top-down
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int uniquePaths(int m, int n) {
vector<vector<int>> memo(m, vector<int>(n, -1));
// 初始化第一行
for (int i=0;i<m; i++)
memo[i][0] = 1;
// 初始化最左行
for (int j=0; j<n; j++)
memo[0][j] = 1;
// top-down
for (int i=1;i<m; i++)
for (int j=1; j<n; j++)
memo[i][j] = memo[i-1][j] + memo[i][j-1];
return memo[m-1][n-1];
}
- Bottom-up
#303 Range Sum Query - Immutable
- 描述
1 | Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive. |
逻辑
从nums[i] 到nums[j] 元素累加就可以了。这种方法对于一条query,没问题。但是query一般频繁的。最好可以把结果提前计算出来,之后的query操作秩序做一次减法,便可以得到结果。所以使用DP。
- 状态定义:mem[0]=0,mem[i]表示从第一个数到第i个数的和。
- 状态转移方程:
memo[i+1] = memo[i] + nums[i]
; - 初始化mem为全零。
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class NumArray2{
private:
vector<int> memo;
public:
NumArray2(vector<int>& nums){
// n+1个元素。
memo = vector<int>(nums.size()+1, 0);
for (int i=0; i<nums.size(); i++)
memo[i+1] = memo[i] + nums[i];
}
// 频繁操作只需求一次减法。效率高
int sumRange(int i, int j){
return memo[j+1] - memo[i];
}
};
#198 House Robber
- 描述
1 | 一条街上有若干房子,现在要抢劫房子,从街头到街尾,所抢的房子不能直接相邻,求可能的最大收益。 |
逻辑
状态定义:mem[i]表示抢[0~i]可获得的最大收益。
状态转移方程:
memo[i] = max(memo[i-1], nums[i]+tmp
;下图为抢house=[5,2,3,1,1,5]的DP过程:
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int rob(vector<int>& nums) {
vector<int> memo(nums.size(), 0);
// 切入口:前两个值特殊处理
memo[0] = nums[0];
memo[1] = max(nums[0], nums[1]);
// 开始切~~
for (int i=2; i<nums.size(); i++){
// 求tmp:当前位置之前可抢劫最大收益(不包括与当前位置紧邻的房子)
int tmp=0;
for (int j=0; j<=i-2; j++)
if (memo[j]>tmp)
tmp = memo[j];
memo[i] = max(memo[i-1], nums[i]+tmp);
}
return memo[nums.size()-1];
}关键:mem[],以初始化的值为切入点,一个一个计算。,