3.26-DP
64. 最小路径和 - 力扣(LeetCode)
思路:
dp[i][j]表示从起点到(i,j)的路径和最小状态转移方程:dp[i+1][j+1]=min(dp[i+1][j],dp[i][j+1])+grid[i][j]
初始值:
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX))
dp[0][1] = 0为什么要在最左边和最上面插入一排0?如果直接从
grid[0][0]出发更新,那么在比较min(dp[0][1] , dp[1][0])时会出现min(INT_MAX , INT_MAX),无法更新。和01背包容量V的最左边插入一排0效果一样。这样循环可以从(0,0)开始写:
for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { dp[i + 1][j + 1] = min(dp[i][j + 1],dp[i + 1][j]) + grid[i][j]; } }答案为 dp[m][n],
写法一
class Solution { public: int minPathSum(vector<vector<int>>& grid) { int m = grid.size() , n = grid[0].size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX)); dp[0][1] = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { dp[i + 1][j + 1] = min(dp[i][j + 1],dp[i + 1][j]) + grid[i][j]; } } return dp[m][n]; } };复杂度分析
- 时间复杂度:O(mn),其中 m 和 n 分别为 grid 的行数和列数。
- 空间复杂度:O(mn)。
M:647. 回文子串
1.确定dp数组(dp table)以及下标的含义
如果大家做了很多这种子序列相关的题目,在定义dp数组的时候 很自然就会想题目求什么,我们就如何定义dp数组。绝大多数题目确实是这样,不过本题如果我们定义,
dp[i]为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系。
dp[i]和dp[i-1],dp[i + 1]看上去都没啥关系。所以我们要看回文串的性质。 如图:
我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。
那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串下标范围[i,j])是否回文依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。
所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。
布尔类型的
dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。2.确定递推公式
当s[i]与s[j]不相等,
dp[i][j]一定是false。当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如aa,也是回文子串
- 情况三:下标:i 与 j 相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串,就看
aba是不是回文就可以了,那么aba的区间就是i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。以上三种情况分析完了,那么递归公式如下:
if(s[i] == s[j]) { if(j - i <= 1){ //情况1,2 res ++; dp[i][j] = true; }else if(dp[i + 1][j - 1]){//情况3 res ++; dp[i][j] == true; } }res为返回结果,统计回文子串的数量。
3.dp数组初始化为false,因此s[i]!=s[j]的情况不用考虑
4.遍历顺序
首先从递推公式中可以看出,情况三是根据
dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。
dp[i + 1][j - 1]在dp[i][j]的左下角,如图:
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的
dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。所以一定要从下到上,从左到右遍历,这样保证
dp[i + 1][j - 1]都是经过计算的。有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证
dp[i + 1][j - 1]都是经过计算的。5.举例推导dp数组
举例,输入:"aaa",
dp[i][j]状态如下:
图中有6个true,所以就是有6个回文子串。
注意因为
dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分。
代码:
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
vector<vector<int>> dp(n , vector<int>(n , 0));
int res = 0;
for (int i = n - 1; i >= 0; i--) {//注意遍历顺序从下到上,j从i开始加保证j >= i
for (int j = i; j < n; j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
res ++;
dp[i][j] = true;
}
}
}
return res;
}
};
5. 最长回文子串 - 力扣(LeetCode)
思路:
有了上题的铺垫,这题在每次更新完
dp[i][j]后记录下全局最长回文子串长度maxlenth和对应的左指针l,最后截取返回即可。
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
vector<vector<int>> dp(n , vector<int>(n , 0));
int maxlength = 0 , l , r;
for (int i = n - 1; i >= 0 ; i--) {
for (int j = i; j < n; j++) {
if(s[i]== s[j] && (j - i <= 1 || dp[i + 1][j - 1])){
dp[i][j] = true;
}
if(dp[i][j] && j - i + 1 > maxlength){
maxlength = j - i + 1;
l = i;
r = j;
}
}
}
return s.substr(l , maxlength);
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
法二:双指针
动态规划的空间复杂度是偏高的。
首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。
在遍历中心点的时候,要注意中心点有两种情况。
一个元素可以作为中心点,两个元素也可以作为中心点。
class Solution { public: int l = 0 , r = 0 , maxlength = 0; string longestPalindrome(string s) { int res = 0; int n = s.size(); auto extend = [&](int i , int j)->void{ while(i >= 0 && j < n && s[i] == s[j]){ if(j - i + 1 > maxlength){ l = i; r = j; maxlength = j - i + 1; } i --; j ++; } }; for (int i = 0; i < n; i++) { extend(i , i); //以i , i 为中心扩散 extend(i , i + 1); //以i , i + 1 为中心扩散 } return s.substr(l , maxlength); } };
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
M:516.最长回文子序列
思路:
回文子串,回文子序列都是动态规划经典题目。回文子串是要连续的,回文子序列可不是连续的!
回文子串系列:
- 647.回文子串 返回的是回文子串的数目,每次更新
dp[i][j]时res++- 5.最长回文子串 返回的是最长子串,每次更新时需要记下左端点l和长度maxlength
- 516.最长回文子序列 返回的是长度
思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点。
动规五部曲分析如下:
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。2.确定递推公式
在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。
如果s[i]与s[j]相同,那么
dp[i][j] = dp[i + 1][j - 1] + 2;如图:
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
因为
dp[i][j]由状态dp[i + 1][j - 1]转移而来:
加入s[j]的回文子序列长度为
dp[i + 1][j]。加入s[i]的回文子序列长度为
dp[i][j - 1]。那么
dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
代码如下:
if (s[i] == s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); }3.dp数组如何初始化
首先要考虑当i 和j 相同的情况,从递推公式:
dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和 j 相同时候的情况。所以需要手动初始化一下,当i与j相同,那么
dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。其他情况
dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0)); for (int i = 0; i < s.size(); i++) dp[i][i] = 1;4.确定遍历顺序
从递归公式中,可以看出,
dp[i][j]依赖于dp[i + 1][j - 1],dp[i + 1][j]和dp[i][j - 1],如图:
所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的。
j的话,可以正常从左向右遍历。
代码如下:
for (int i = n - 1; i >= 0; i--) { for (int j = i + 1; j < n; j++) { if (s[i] == s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); } } }5.举例推导dp数组
输入s:"cbbd" 为例,dp数组状态如图:
红色框即:
dp[0][s.size() - 1]; 为最终结果。如果是回文子串求最大长度,那么要定义全局变量maxlen,在过程中取最大值,因为dp数组是布尔类型,保存的是ij之间是否回文,而不是长度。如果定义为长度,状态转移及其复杂。
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.length();
vector<vector<int>> dp(n , vector<int>(n , 0));
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int i = n - 1; i >= 0; i --) {
for (int j = i + 1; j < n; j++) {
if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
else {
dp[i][j] = max(dp[i + 1][j] , dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
};
经典线性 DP
§4.1 最长公共子序列(LCS)
一般定义 f[i][j] 表示对 (s[:i],t[:j]) 的求解结果。
1143. 最长公共子序列 - 力扣(LeetCode)
灵神的思路:




最终可以得到递推关系式:
\(d f s(i, j)=\left\{\begin{array}{ll} d f s(i-1, j-1)+1 & s[i]=t[j] \\ \max (d f s(i-1, j), d f s(i, j-1)) & s[i] \neq t[j] \end{array}\right.\)
1.确定dp数组以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1 与 长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么?
这样定义是为了后面代码实现方便,如果非要定义为长度为[0, i]的字符串text1也可以,我在 动态规划:718. 最长重复子数组 (opens new window)中的「拓展」里 详细讲解了区别所在,其实就是简化了dp数组第一行和第一列的初始化逻辑。
2.确定递推公式
主要就是两大情况: text1[i - 1] == text2[j - 1]和text1[i - 1] != text2[j - 1]
如果
text1[i - 1]与text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;如果
text1[i - 1]与text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);代码如下:
if (text1[i - 1] == text2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); }3.dp数组如何初始化
先看看
dp[i][0]应该是多少呢?
test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
代码:
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));4.确定遍历顺序
从递推公式,可以看出,有三个方向可以推出
dp[i][j],如图:
那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size() , n = text2.size();
vector<vector<int>> dp(m + 1 , vector<int>(n + 1 , 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//记住定义:dp[i][j]:长度为[0, i - 1]的字符串text1 与 长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j],因此比较的是text[i - 1]与text2[j - 1]
if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else{
dp[i][j] = max(dp[i][j - 1] , dp[i - 1][j]);
}
}
}
return dp[m][n];
}
};
72. 编辑距离 - 力扣(LeetCode)
思路:
m = word1.size() , n = word2.size()
考虑word1[i] 与 word2[j] 是否相等的情况
- s[i] == t[j]时
dp[i][j] = dp[i - 1][j - 1]- s[i] != t[j]
dp[i][j] = min(dp[i , j - 1] , dp[i - 1][j] , dp[i - 1][j - 1])编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离。
1. 确定dp数组(dp table)以及下标的含义
dp[i][j]表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。2. 确定递推公式
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
if (word1[i - 1] == word2[j - 1]) // 那么说明不用任何编辑,不操作 if (word1[i - 1] != word2[j - 1]) 增 删 换
if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?
操作一:word1删除一个元素,那么就是以下标
i - 2为结尾的word1 与j-1为结尾的word2的最近编辑距离 再加上一个操作。即dp[i][j] = dp[i - 1][j] + 1;操作二:word2删除一个元素,那么就是以下标
i - 1为结尾的word1 与j-2为结尾的word2的最近编辑距离 再加上一个操作。即dp[i][j] = dp[i][j - 1] + 1;这里有同学发现了,怎么都是删除元素,添加元素去哪了。
word2添加一个元素,相当于word1删除一个元素,例如
word1 = "ad" ,word2 = "a",word1删除元素'd'和word2添加一个元素'd',变成word1="a", word2="ad", 最终的操作数是一样! dp数组如下图所示意的:a a d +-----+-----+ +-----+-----+-----+ | 0 | 1 | | 0 | 1 | 2 | +-----+-----+ ===> +-----+-----+-----+ a | 1 | 0 | a | 1 | 0 | 1 | +-----+-----+ +-----+-----+-----+ d | 2 | 1 | +-----+-----+
- 操作三:替换元素,
word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。可以回顾一下,
if (word1[i - 1] == word2[j - 1])的时候我们的操作 是dp[i][j] = dp[i - 1][j - 1]对吧。那么只需要一次替换的操作,就可以让
word1[i - 1]和word2[j - 1]相同。所以
dp[i][j] = dp[i - 1][j - 1] + 1;综上,当
if (word1[i - 1] != word2[j - 1])时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;三个或三个以上的数取max、min,可用大括号括起来{},这个版本接受一个
初始化器列表递归公式代码如下:
if (word1[i - 1] == word2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; }
3. dp数组如何初始化
dp[i][0]:以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。那么
dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0]= i;同理
dp[0][j]= j;所以C++代码如下:
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;4. 确定遍历顺序
可以看出
dp[i][j]是依赖左方,上方和左上方元素的,如图:
所以在dp矩阵中一定是从左到右从上到下去遍历。
代码如下:
for (int i = 1; i <= word1.size(); i++) { for (int j = 1; j <= word2.size(); j++) { if (word1[i - 1] == word2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; } } }5. 举例推导dp数组
代码如下:
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size() , n = word2.size();
vector<vector<int>> dp(m + 1 , vector<int>(n + 1 , INT_MAX));
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j],因此比较的是word1[i - 1]与word2[j - 1]
if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else{
dp[i][j] = min({dp[i][j - 1] , dp[i - 1][j] , dp[i - 1][j - 1]}) + 1;
}
}
}
return dp[m][n];
}
};











浙公网安备 33010602011771号