写一些自己LeetCode的刷题过程及总结06
##leetcode上有2000+的题,不可能都刷完,我的刷题顺序是先分类型,然后再分难度,不断提升,当然过程中也参考了其他大神们的一些建议,光刷题收获不大,最重要的还是不断归纳总结,没事刷两道,坚持写总结其实也挺有意思的。##
##还在不断更新总结!##
##本文仅用来记录自己平时的学习收获##
##有朝一日我也能写出漂亮的代码!##
一、动态规划
1.1 关于动态规划
动态规划(Dynamic Programming, 简称DP),如果某一个问题有很多重叠子问题,那么可以考虑使用动态规划,动态规划中每一个状态一定是从上一个状态中推导出来的。
动态规划问题的⼀般形式就是求最值。动态规划其实是运筹学的⼀种最优化方法,只不过在计算机问题上应⽤⽐较多,⽐如说让你求最⻓递增⼦序列,最小编辑距离等等。 既然是要求最值,核⼼问题是什么呢?求解动态规划的核⼼问题是穷举。因为要求最值,肯定要把所有可⾏的答案穷举出来,然后在其中找最值。
下面给出求解动态规划问题的思考步骤:
动规五步:
1、确定dp数组及下标的含义;
2、确定递推公式;
3、如何初始化dp数组;
4、确定遍历顺序;
5、举例推导dp数组(在草稿本上完成)。
1.2 leetcode部分动态规划题目及代码
1.2.1 动规基础题目
509.斐波那契数列
70.爬楼梯
746.使用最小花费爬楼梯
62.不同路径
63.不同路径II
343.整数拆分
96.不同的二叉搜索树
1、509.斐波那契数列
//这道题可以说是动规典型的入门例子了
class Solution {
public:
int fib(int n) {
if (n == 0) return 0;
//动规五步:
//1、确定dp数组及下标含义
//dp[i]的定义为:第i个数的斐波那契值是dp[i]
vector<int> dp(n + 1, 0);
//3、dp数组如何初始化
//dp[0] = 0, dp[1] = 1
dp[1] = 1;
//4、确定遍历顺序
//从递推公式中可以看出dp[i]的值依赖dp[i-1]和dp[i-2]所以一定是从前向后遍历
for (int i = 2; i <= n; ++i) {
//2、确定递推公式
//状态转移方程:dp[i] = dp[i-1] + dp[i-2]
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
//其实这道题只需要两个中间变量就可,不需要维护整个dp数组
class Solution {
public:
int fib(int n) {
if (n <= 1) return n;
int n0 = 0;
int n1 = 1;
int ret = 0;
for (int i = 2; i <= n; ++i) {
ret = n0 + n1;
n0 = n1;
n1 = ret;
}
return ret;
}
};
2、70.爬楼梯
//这道题其实就是斐波那契数列的变形,同样也可以优化空间,这里不再写了
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) return n;
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:爬到第i阶楼梯总共有多少中方法
vector<int> dp(n + 1, 0);
//3、dp数组如何初始化
//dp[0] = 0, dp[1] = 1
dp[1] = 1;
dp[2] = 2;
//4、确定遍历顺序
//dp[i]的确定依赖dp[i-1]和dp[i-2],所以从前向后遍历
for (int i = 3; i <= n; ++i) {
//2、确定递推公式
//要爬到第i个台阶,要么从第i-1层台阶往上爬一层,要么从第i-2层台阶往上爬两层,
//所以dp[i] = dp[i-1] + dp[i-2]
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
3、746.使用最小花费爬楼梯
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//动规五步
//1、确定dp数组及下标含义
//dp[i]表示:爬上第i个台阶时的最小花费
vector<int> dp(cost.size(), 0);
//3、dp数组如何初始化
//dp[0] = cost[0],dp[1] = cost[1]
dp[0] = cost[0];
dp[1] = cost[1];
//4、确定遍历顺序:从前向后
for (int i = 2; i < cost.size(); ++i) {
//2、确定递推公式
//有两个途径到dp[i],分别是dp[i-1]和dp[i-2]
//对它俩取最小后加上到达第i个台阶的体力花费
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
//根据题意,到达楼层顶部时可理解为不花费体力,
//所以取倒数第一步和倒数第二步的最小值返回
return min(dp[cost.size() - 1], dp[cost.size() - 2]);
}
};
4、62.不同路径
class Solution {
public:
int uniquePaths(int m, int n) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i][j]表示:到达(i,j)处共有多少条路径
vector<vector<int>> dp(m, vector<int> (n, 0));
//3、如何初始化dp数组
//在第一行中只能从左边来,在第一列中只能从上边来,所以都初始化为1
for (int i = 0; i < m; ++i) dp[i][0] = 1;
for (int j = 0; j < n; ++j) dp[0][j] = 1;
//4、遍历顺序:从上到下,从左到右
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
//2、确定递推公式
//要到达(i,j)处,要么从上方(i-1, j)处来,要么从左边(i, j-1)处来
dp[i][j] = dp[i-1][j] + dp[i][j - 1];
//5、举例模拟
}
}
return dp[m - 1][n - 1];
}
};
5、63.不同路径II
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i][j]表示:到达(i,j)处共有多少条路径
vector<vector<int>> dp(obstacleGrid.size(), vector<int> (obstacleGrid[0].size(), 0));
//3、如何初始化dp数组
//第一行和第一列都只有一种走法,都初始化为1,但遇到障碍物打断
for (int i = 0; i < obstacleGrid.size(); ++i) {
if (obstacleGrid[i][0] == 1) break;
dp[i][0] = 1;
}
for (int j = 0; j < obstacleGrid[0].size(); ++j) {
if (obstacleGrid[0][j] == 1) break;
dp[0][j] = 1;
}
//4、遍历顺序:从上到下,从左到右
for (int i = 1; i < obstacleGrid.size(); ++i) {
for (int j = 1; j < obstacleGrid[0].size(); ++j) {
//2、确定递推公式
//当(i,j)处不为障碍物时,要到达(i,j)处,要么从(i-1,j)处来,要么从(i,j-1)处来
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
} else continue;
}
}
return dp[obstacleGrid.size() - 1][obstacleGrid[0].size() - 1];
}
};
6、343.整数拆分
class Solution {
public:
int integerBreak(int n) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:拆分数字i可得到的最大乘积为dp[i]
vector<int> dp(n + 1, 0);
//3、如何初始化dp数组
//dp[0]和dp[1]没有意义,所以初始化dp[2] = 1
dp[2] = 1;
//4、遍历顺序:
//要计算dp[i]先要计算dp[i-j],所以i从前向后遍历;
//当数为0或1时没有意义无法拆分,所以i-j > 1,所以j < i - 1
for (int i = 3; i <= n; ++i) {
for (int j = 1; j < i - 1; ++j) {
//2、确定递推公式
//要得到dp[i]:要么直接拆分j * (i - j);要么j * dp[i - j](多次拆分)
dp[i] = max(dp[i], max(dp[i - j] * j, (i - j) * j));
//5、举例推导
}
}
return dp[n];
}
};
7、96.不同的二叉搜索树
//这道题还是挺难理解的、感觉可以算困难题了
class Solution {
public:
int numTrees(int n) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:1到i为节点组成的二叉搜索树的个数为dp[i]
vector<int> dp(n + 1, 0);
//3、如何初始化dp数组
//因为后续用的是乘法,所以dp[0] = 1
dp[0] = 1;
//4、遍历顺序:从前向后
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
//2、确定递推公式
//dp[i] += dp[以j为头节点的左子树节点数量] * dp[以j为头节点的右子树节点数量]
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
};
1.2.2 背包问题
动态规划中背包问题的种类有很多,但据说对于面试的话掌握01背包和完全背包就够了,至于其它背包问题都是竞赛级别的,听说leetcode上也没有这类题目。
至于什么是01背包什么是完全背包,我在这里就先不解释了,我也正处于学习摸索的阶段,想了解的话不如去看看其他大佬们的文章。在这里就先只记录一下背包问题的相关题目,等日后自己真的吃透了再来总结补充。
1.2.2.1 01背包
416.分割等和子集
1049.最后一块石头的重量II
494.目标和
从这里开始的题就比较难了,我也是在参考了其他人的代码后尽量用自己的理解写出注释。
1、416.分割等和子集
class Solution {
public:
bool canPartition(vector<int>& nums) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:背包容量为i时凑成的子集总和为dp[i](当dp[i]==i时才说明可以凑出i,当dp[i]<i时说明凑不出i,这里可在第五步通过自己举例推导得出结论)
//3、如何初始化dp数组
//题中说每个数组的元素不会超过100,数组长度不会超过200
//那么总和不会超过20000,所以背包最大只需要总和的一半,即10001
vector<int> dp(10001, 0);
int sum = 0;
for (int num : nums) sum += num;
if (sum % 2 == 1) return false;
int target = sum / 2;
//4、确定遍历顺序
//如果使用一维数组,物品遍历在外环,背包遍历在内环并且倒叙遍历
for (int i = 0; i < nums.size(); ++i) {
for (int j = target; j >= nums[i]; --j) {
//2、确定递推公式
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[target] == target) return true;
return false;
}
};
2、1049.最后一块石头的重量II
//转换成 01 背包问题(考虑到石头要么加要么减,所以可以 " 分成两堆 ",问两堆最小的差值。
//那么问题就变成了一个简单的背包),求两堆石头的最小差值。
//sum / 2:要求差值最小,越接近 sum/2 越则差值就越小。
//于是就想象成有一个背包最多能装 sum/2 的石头,看在不超过 sum/2 的范围最多能装多少石头。
//背包容量:target=sum/2,当背包容量为target时最多能装下多重的石头。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:当背包容量为i时,所装石头的最大重量为dp[i]
//3、如何初始化dp数组
vector<int> dp(15001, 0);
int sum = 0;
for (int stone : stones) sum += stone;
int target = sum / 2;
//4、遍历顺序
for (int i = 0; i < stones.size(); ++i) {
for (int j = target; j >= stones[i]; --j) {
//2、确定递推公式
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
//将石头分成了两堆,一堆是dp[target],另一堆是sum-dp[target]
//因为target=sum/2是向下取整,所以sum-dp[target]一定大于等于dp[target]
return sum - dp[target] - dp[target];
}
};
通过这几道题可以发现:01背包的模板几乎是完全相同的,只是在最后返回时需要根据题意进行不同处理。
3、494.目标和
//我们想要的 S = 正数和 - 负数和 = x - y
//而已知 x 与 y 的和是数组总和:x + y = sum
//可以求出 x = (S + sum) / 2 = target
//此时问题转化为:装满容量为x的背包,有几种方法
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int num : nums) sum += num;
if (target > sum || (target + sum) % 2 == 1) return 0;
int bagSize = (target + sum) / 2;
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:填满容量为i(包括i)的包,有dp[i]种方法
vector<int> dp(bagSize + 1, 0);
//3、如何初始化dp数组
//dp[0] = 1很好理解,装满容量为0的包,只有一种方法
dp[0] = 1;
//4、确定遍历顺序
for (int i = 0; i < nums.size(); ++i) {
for (int j = bagSize; j >= nums[i]; --j) {
//2、确定递推公式
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
1.2.2.2 完全背包
518.零钱兑换II
279.完全平方和
1、518.零钱兑换II
class Solution {
public:
int change(int amount, vector<int>& coins) {
//动规五步:
//1、确定dp数组下标及含义
//dp[j]表示:凑成总金额j的货币组合数为dp[j]
vector<int> dp(amount + 1, 0);
//3、如何初始化dp数组
//dp[0]指凑成金额0的货币组合数为1
dp[0] = 1;
//4、确定遍历顺序
for (int i = 0; i < coins.size(); ++i) {//遍历物品
for (int j = coins[i]; j <= amount; ++j) {//遍历背包
//2、确定递推公式
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
2、279.完全平方和
class Solution {
public:
int numSquares(int n) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:和为i的完全平方数的最少数量为dp[i]
vector<int> dp(n + 1, INT_MAX);
//3、如何初始化dp数组
//dp[0]表示:和为0的完全平方数的最少数量为0
dp[0] = 0;
//4、确定遍历顺序
for (int i = 1; i * i <= n; ++i) {//遍历物品
for (int j = 1; j <= n; ++j) {//遍历背包
//2、确定递推公式
if (j - i * i >= 0) {
dp[j] = min(dp[j - i * i] + 1, dp[j]);
}
}
}
return dp[n];
}
};
小总结:01背包与完全背包的区别,当一个物品可以重复使用(即可以重复放入背包),是完全背包问题;当一个物品只能放入一次,是01背包问题。
01背包的核心就是选或者不选。
1.2.3 打家劫舍问题
1.2.4 股票问题
1.2.5 子序列和子串问题
首先请记住子序列和子串的意思,别弄混了。。。
子串:原序列中必须连续的一段
子序列:原序列中可以不连续的一段
300.最长递增子序列
673.最长递增子序列个数
674.最长连续递增序列
718.最长重复子数组
1143.最长公共子序列
1035.不相交的线
53.最大子序和
392.判断子序列
115.不同的子序列
583.两个字符串的删除操作
72.编辑距离
647.回文子串
5.最长回文子串
516.最长回文子序列
1、300.最长递增子序列
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:i之前包括i的最长上升子序列的长度为dp[i]
//3、如何初始化dp数组:全部初始化为1
vector<int> dp(nums.size(), 1);
int maxLen = 1;
//4、确定遍历顺序
for (int i = 1; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
//2、确定递推公式
//位置i的最长上升子序列等于j从0到i-1处的最长上升子序列长度+1
dp[i] = max(dp[i], dp[j] + 1);
maxLen = max(maxLen, dp[i]);
}
}
}
return maxLen;
}
};
2、673.最长递增子序列个数
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:从0到i包含i处的最长连续递增子序列的长度是dp[i]
//count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]
//3、如何初始化dp数组:全部初始化为1
vector<int> dp(nums.size(), 1);
vector<int> count(nums.size(), 1);
int maxLen = 1;
//4、确定遍历顺序
for (int i = 1; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
//2、确定递推公式
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;//说明找到了一个更长的
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
}
if (maxLen < dp[i]) maxLen = dp[i];
}
}
int ret = 0;
for (int i = 0; i < dp.size(); ++i) {
if (maxLen == dp[i]) ret += count[i];
}
return ret;
}
};
3、674.最长连续递增序列
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i]表示:从0到i包含i的最长连续递增序列的长度是dp[i]
//3、如何初始化dp数组:全初始化为1
vector<int> dp(nums.size(), 1);
int maxLen = 1;
//4、确定遍历顺序
for (int i = 1; i < nums.size(); ++i) {
if (nums[i - 1] < nums[i]) {
//2、确定递推公式
dp[i] = dp[i - 1] + 1;
maxLen = max(maxLen, dp[i]);
}
}
return maxLen;
}
};
4、718.最长重复子数组
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
//动规五步:
//1、确定dp数组及下标含义
//dp[i][j]表示:下标为i-1结尾的A和下标为j-1结尾的B最长重复子数组长度为dp[i][j]
vector<vector<int>> dp(nums1.size() + 1, vector<int> (nums2.size() + 1, 0));
//3、如何初始化dp数组
//dp[0][j] = 0; dp[i][0] = 0;
for (int i = 0; i < dp.size(); ++i) dp[i][0] = 0;
for (int j = 0; j < dp[0].size(); ++j) dp[0][j] = 0;
int ret = 0;
//4、确定遍历顺序
for (int i = 1; i <= nums1.size(); ++i) {
for (int j = 1; j <= nums2.size(); ++j) {
//2、确定递推公式
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
ret = max(ret, dp[i][j]);
}
}
return ret;
}
};
5、1143.最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
//动规五步
//1、确定dp数组下标及含义
//dp[i][j]表示:长度为0~i-1的字符串test1与长度为0~j-1的字符串text2的最长公共子序列为dp[i][j]
vector<vector<int>> dp(text1.size() + 1, vector<int> (text2.size() + 1, 0));
//3、如何初始化dp数组
//全部初始化为0
//4、确定遍历顺序
for (int i = 1; i <= text1.size(); ++i) {
for (int j = 1; j <= text2.size(); ++j) {
//2、确定递推公式
if (text1[i - 1] == text2[j - 1]) {
//如果text1[i - 1] == text2[j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
//如果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]
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.size()][text2.size()];
}
};
6、1035.不相交的线
//其实就是求两个数组最长公共子序列的长度,跟上一道题一模一样
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:0~i-1的nums1与0~j-1的nums2可画出的不相交的线有dp[i][j]条
vector<vector<int>> dp(nums1.size() + 1, vector<int> (nums2.size() + 1, 0));
//3、如何初始化dp数组
//全部初始化为0
//4、确定遍历顺序
for (int i = 1; i <= nums1.size(); ++i) {
for (int j = 1; j <= nums2.size(); ++j) {
//2、确定递推公式
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[nums1.size()][nums2.size()];
}
};
7、53.最大子序和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) return 0;
//动规五步:
//1、确定dp数组下标及含义
//dp[i]表示:下标0~i的最大连续子序和为dp[i]
vector<int> dp(nums.size(), 0);
//3、如何初始化dp数组
dp[0] = nums[0];
int maxSum = nums[0];
//4、确定遍历顺序
for (int i = 1; i < nums.size(); ++i) {
//2、确定递推公式
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
maxSum = max(dp[i], maxSum);
}
return maxSum;
}
};
8、392.判断子序列
//与1143.最长公共子序列类似
class Solution {
public:
bool isSubsequence(string s, string t) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:下标为0~i-1的s和下标为0~j-1的t,相同子序列的长度为dp[i][j]
vector<vector<int>> dp(s.size() + 1, vector<int> (t.size() + 1, 0));
//3、如何初始化dp数组
//全部初始化为0
//4、确定遍历顺序
for (int i = 1; i <= s.size(); ++i) {
for (int j = 1; j <= t.size(); ++j) {
//2、确定递推公式
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
//此时相当于要从t中删除当前元素
dp[i][j] = dp[i][j - 1];
}
}
}
if (dp[s.size()][t.size()] == s.size()) return true;
return false;
}
};
9、115.不同的子序列
class Solution {
public:
int numDistinct(string s, string t) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:以i-1结尾的s子序列中出现以j-1结尾的t的个数为dp[i][j]
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t> (t.size() + 1, 0));
//3、如何初始化dp数组
//dp[i][0]表示以i-1结尾的s可以随便删除元素,出现空字符串的个数,初始化为1
//dp[0][j]表示空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数,初始化为0
for (int i = 0; i <= s.size(); ++i) dp[i][0] = 1;
//4、确定遍历顺序
for (int i = 1; i <= s.size(); ++i) {
for (int j = 1; j <= t.size(); ++j) {
//2、确定递推公式
if (s[i - 1] == t[j - 1]) {
//当相等时有两种可能:用s[i-1]来匹配;不用s[i-1]来匹配
//用s[i-1]来匹配,个数为dp[i - 1][j - 1]
//不用s[i-1]来匹配,个数为dp[i - 1][j]
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.size()][t.size()];
}
};
10、583.两个字符串的删除操作
//跟上一道题有些类似,不过这里两个字符串都可以进行删除
class Solution {
public:
int minDistance(string word1, string word2) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:以i-1结尾的字符串Word1和以j-1结尾的字符串Word2,想要达到相等,所需要删除的元素的最少次数为dp[i][j]
vector<vector<int>> dp(word1.size() + 1, vector<int> (word2.size() + 1, 0));
//3、如何初始化dp数组
//dp[i][0]表示要对Word1删除多少个元素才能与空串Word2相同,所以初始化为i
for (int i = 1; i <= word1.size(); ++i) dp[i][0] = i;
for (int j = 1; j <= word2.size(); ++j) dp[0][j] = j;
//4、确定遍历顺序
for (int i = 1; i <= word1.size(); ++i) {
for (int j = 1; j <= word2.size(); ++j) {
//2、确定单次递归逻辑
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
//不同时:
//情况一:删除Word1[i-1],最少操作次数为dp[i-1][j] + 1
//情况二:删除Word2[j-1],最少操作次数为dp[i][j-1] + 1
//情况三:同时删除word1[i-1]和word2[j-1],最少操作次数为dp[i-1][j-1] + 2
dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2});
}
}
}
return dp[word1.size()][word2.size()];
}
};
11、72.编辑距离
//同样,这道题也与上一道题类似,不仅可以进行删除操作,还可以添加、替换
class Solution {
public:
int minDistance(string word1, string word2) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:以下标i-1结尾的Word1和以下标j-1结尾的Word2,最近编辑距离为dp[i][j]
vector<vector<int>> dp(word1.size() + 1, vector<int> (word2.size() + 1, 0));
//3、如何初始化dp数组
for (int i = 1; i <= word1.size(); ++i) dp[i][0] = i;
for (int j = 1; j <= word2.size(); ++j) dp[0][j] = j;
//4、确定遍历顺序
for (int i = 1; i <= word1.size(); ++i) {
for (int j = 1; j <= word2.size(); ++j) {
//2、确定单次递归逻辑
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
//三种情况:
//情况一:Word1增加一个元素,使word1[i-1]与word2[j-1]相同,就是以i-2结尾的Word1与j-1结尾的Word2的最近编辑距离上加一,即dp[i][j] = dp[i-1][j] + 1
//情况二:Word2增加一个元素,使word[i-1]与word2[j-1]相同,就是以i-1结尾的Word1与j-2结尾的Word2的最近编辑距离上加一,即dp[i][j] = dp[i][j - 1] + 1
//其实对一个字符串添加一个元素就相当于对另一个字符串删除一个元素,操作数是相同的
//情况三:替换,dp[i][j] = dp[i-1][j-1] + 1
dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};
12、647.回文子串
class Solution {
public:
int countSubstrings(string s) {
//动规五步:
//1、确定dp数组下标及含义
//dp[i][j]表示:范围[i,j]内的子串是否是回文子串,如果是dp[i][j]为true
vector<vector<bool>> dp(s.size(), vector<bool> (s.size(), false));
//3、如何初始化dp数组
//全部初始化为false
int result = 0;
//4、确定遍历顺序
//从递推公式中可以看出dp[i][j]取决于dp[i+1][j-1],它在dp[i][j]的左下角
//所以在遍历时必须先知道左下角的值,即应该从下到上,从左到右遍历
for (int i = s.size() - 1; i >= 0; --i) {
for (int j = i; j < s.size(); ++j) {
//2、确定单次递归逻辑
if (s[i] == s[j]) {
if (j - i <= 1) {
++result;
dp[i][j] = true;
} else {
if (dp[i + 1][j - 1]) {
++result;
dp[i][j] = true;
}
}
}
}
}
return result;
}
};
13、5.最长回文子串
//这道题在之前的文章中也给出了双指针解法
class Solution {
public:
string longestPalindrome(string s) {
//动规五步
//1、确定dp数组下标及含义
//dp[i][j]表示:范围[i,j]内的子串是否是回文子串
vector<vector<bool>> dp(s.size(), vector<bool> (s.size(), false));
//3、如何初始化dp数组
int maxLen = 0;
int index = 0;
//4、确定遍历顺序
for (int i = s.size() - 1; i >= 0; --i) {
for (int j = i; j < s.size(); ++j) {
//2、确定单次递归逻辑
if (s[i] == s[j]) {
if (j - i <= 1) {
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) {
dp[i][j] = true;
}
}
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
index = i;
}
}
}
return s.substr(index, maxLen);
}
};
14、516.最长回文子序列
class Solution {
public:
int longestPalindromeSubseq(string s) {
//动规五步
//1、确定dp数组下标及含义
//dp[i][j]表示:范围[i,j]内的最长回文子序列的长度是dp[i][j]
vector<vector<int>> dp(s.size(), vector<int> (s.size(), 0));
//3、如何初始化dp数组
//对角线初始化为1
for (int i = 0; i < s.size(); ++i) dp[i][i] = 1;
//4、确定遍历顺序
for (int i = s.size() - 1; i >= 0; --i) {
for (int j = i + 1; j < s.size(); ++j) {
//2、确定单次递归逻辑
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]);
}
}
}
//注意dp数组下标的含义!
return dp[0][s.size() - 1];
}
};