动态规划
1. 509. 斐波那契数
class Solution {
public:
int fib(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
return fib(n -1) + fib(n - 2);
}
};
比较简单
2. 70. 爬楼梯
class Solution {
public:
int climbStairs(int n) {
if(n < 3) return n;
return climbStairs(n - 1) + climbStairs(n - 2);
}
};
class Solution {
public:
int climbStairs(int n) {
if(n < 3) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i < n + 1; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
这题递归做法会直接超时,递归的时间复杂度为2的n次方,所以只能用for常规循环来做。
第二种做法就是使用for循环将整个数组都求出来,然后根据需要返回相应的值即可。
3. 746. 使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/min-cost-climbing-stairs
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size()+1);
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i < dp.size(); i++){
dp[i] = min(dp[i - 1] + cost[i - 1],dp[i - 2] + cost[i - 2]);
}
return dp[cost.size()];
}
};
这题总的来说也不难,就是题目有些奇怪,他要求的是最终到第n阶台阶。但是cost数组中的费用是到n - 1阶台阶的。
4. 62.不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0; i < n; i++) dp[0][i] = 1;
for(int i = 0; i < m; i++) dp[i][0] = 1;
// for (int i = 0; i < m; i++) dp[i][0] = 1;
// for (int j = 0; j < n; j++) dp[0][j] = 1;
for(int i = 1; i <= m-1; i++){
for(int j = 1; j <= n-1; j++){
dp[i][j] = dp[i - 1][j] +dp[i][j -1 ];
}
}
return dp[m - 1][n - 1];
}
};
也是直接推导状态转移方程即可。
本题也还可以用数论的方法写,就是高中学过的组合问题了
5. 63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m,vector<int>(n,-1));
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(obstacleGrid[i][j] == 1){
cout<<"in"<<endl;
dp[i][j] = 0;
}
}
}
for(int i = 0; i < m; i++){
if(i == 0){
if(obstacleGrid[i][0] == 1) dp[i][0] = 0;
else dp[i][0] = 1;
continue;
}
if(obstacleGrid[i][0] == 1){
dp[i][0] = 0;
}else{
dp[i][0] = dp[i-1][0];
}
}
for(int i = 0; i < n; i++){
if(i == 0){
if(obstacleGrid[0][i] == 1) dp[0][i] = 0;
else dp[0][i] = 1;
continue;
}
if(obstacleGrid[0][i] == 1){
dp[0][i] = 0;
}else{
dp[0][i] = dp[0][i-1];
}
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
printf("%d ",dp[i][j]);
}
printf("\n");
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(dp[i][j] == -1){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
printf("%d ",dp[i][j]);
}
printf("\n");
}
return dp[m-1][n-1];
}
};
其实是和上题一样的状态转移方程,只是初始化需要做一些工作,将有障碍的地方全部初始化为0即可。
6.343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[1] = 1;
// 状态转移方程
for(int i = 2; i <= n; i++){
int ss = 0;
for(int j = 1; j < i; j++){
int temp = max(j*(i-j),j*dp[i-j]);
if(temp>ss) ss = temp;
}
dp[i] = ss;
}
for(int i = 1; i <=n;i++){
printf("%d ",dp[i]);
}
return dp[n];
}
};
dp[i]表示将正整数 i 拆分成至少两个正整数的和之后,这些正整数的最大乘积。特别地,0 不是正整数,1 是最小的正整数,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0。
当 i≥2 时,假设对正整数 i 拆分出的第一个正整数是 j(1≤j<i),则有以下两种方案:
将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j×(i−j);
将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j]。
因此,当 j 固定时,有dp[i]=max(j×(i−j),j×dp[i−j])。由于 j 的取值范围是 1 到 i−1,需要遍历所有的 j 得到 dp[i] 的最大值,因此可以得到状态转移方程如下:
dp[i] = max{max(j×(i−j),j×dp[i−j])}
最终得到 dp[n] 的值即为将正整数 n 拆分成至少两个正整数的和之后,这些正整数的最大乘积。
每个dp[i]的值都是从能拆分为几个数来分类计算的。
7. 96.不同的二叉搜索树
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1,0);
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
本题最开始我在自己推导的时候想的是从dp[i-1] 插入一个i节点(这部分称为A
),那么这部分肯定算是dp[i]的一部分,由于A部分的根节点全部是属于1~i-1的,所以认为第二部分B部分
应该是以i作为根节点的,当i作为根节点,那么其余的节点肯定都在其左子树,对左子树进行数量计算的话,那么也同样是dp[i-1] , 但是最后发现从i=3的时候就不对了,因为i节点并不一定是要最后一个插入到搜索树的,所以我陷入的误区是在这里。
通过其他人的思路,可以发现比较重要的一点是,不要拘泥于元素是哪些,而是从元素个数出发计算二叉搜索树的数量,说来也惭愧,这本就是本题的问题,居然没有意识到,认识到这一点后,对于dp[i]的计算,可以从分别以j(j属于1~i)
为根结点的二叉搜索树来计算。而对于本题的搜索树来说,当根节点确定了,那么左子树和右子树的节点数也确定了。从而可以容易计算出该根节点所有的二叉搜索树的数量。并最终确定状态转移方程:dp[i] += dp[j-1]*dp[i-j];
8. 416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
if (sum % 2 == 1) return false;
int target = sum / 2; // 求出背包容量
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
这里是由于每件物品的价值和重量是相等的,所以当背包容量为target的时候,要想元素的价值也达到target,那么只有尽可能往里面放价值总量最大的物品,所以这里就可以用max来求,在本题的设置下,value≤target的。
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/last-stone-weight-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
// 找到重量相等的两堆
// 元素的重量 价值 都是石头重量
// 背包大小为 sum / 2
//
int sum = 0;
for(auto it : stones){
sum+=it;
}
int target = sum/2;
// 0 1 2 ... target
vector<int> dp(target+1);
for(int i = 0; i < stones.size(); i++){ // 先枚举物品
for(int j = target; j >= stones[i]; j--){ // 倒序更新数组
dp[j] = max(dp[j],dp[j - stones[i]] + stones[i]);
}
for(auto it : dp){
cout<<it<<" ";
}
cout<<endl;
}
return sum - dp[target]*2;
}
};
发现有些理解错算法笔记上的内容了,dp[i]还是表示的是当容量为i的时候能装载的最大价值,而不是所有货物的容量恰好为i。
另外一个就是本题转化为01背包问题,开始时确实有点难以理解,为啥这可以转化成背包问题,后面理解了就好些了,然后按照01背包的模板做就行。
10.494. 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int ans = 0;
int sum = 0;
int tar;
int findTargetSumWays(vector<int>& nums, int target) {
tar = target;
backtracking(nums,0);
return ans;
}
void backtracking(vector<int>& nums,int index){
if(index == nums.size()){
if(sum == tar){
ans++;
}
return;
}
sum+=nums[index];
backtracking(nums,index+1);
sum-=nums[index];
sum-=nums[index];
backtracking(nums,index+1);
sum+=nums[index];
}
};
class Solution {
public:
int count = 0;
int findTargetSumWays(vector<int>& nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
void backtrack(vector<int>& nums, int target, int index, int sum) {
if (index == nums.size()) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
};
// dp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if (abs(S) > sum) return 0; // 此时没有方案
if ((S + sum) % 2 == 1) return 0; // 此时没有方案
int bagSize = (S + sum) / 2;
vector<int> dp(bagSize + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wGLdLQu0-1651654787113)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/073d9fcb-d763-432c-a724-2d00a6e56527/Untitled.png)]
这题用回溯法暴力搜索的话,直接超时了。
但是发现官方的不会超时
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3VZGwMH-1651654787114)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1820c2fe-1f31-4a25-987a-303f693267c3/Untitled.png)]
居然是加减加减这一点时间引发了超时
状态转移方程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lcJ5uHC-1651654787114)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/39e8e067-047e-4899-89d8-c0357ebedd77/Untitled.png)]
这题和前面的题目最大的不同是求的是组合的个数,并且dp[i]表示的正好是重量等于i的情况。
11.474.一和零
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
// dp[i][j]表示有i个0和j个1的背包能装下的最大子集数量
// 全部初始化为0
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int k = 0; k < strs.size(); k++){
int i0=0,j0=0;
for(auto it : strs[k]){
if((it - '0')==0) i0++;
else j0++;
}
for(int i = m; i >=i0; i--){
for(int j = n; j >=j0; j--){
dp[i][j] = max(dp[i][j],dp[i-i0][j-j0]+1);
}
}
}
return dp[m][n];
}
};
本题其实还是01背包问题,只是背包多了一个维度,既然多了一个维度,那么原先用的一维数组也要升级成为二维数组了,dp[i][j]表示有i个0和j个1的背包能装下的最大子集数量 。对于前k个元素,分别计算每一轮的dp,最终得到的dp[m][n]即为要求的答案。
本题的状态转移方程dp[i][j] 的含义:
- 如果当前元素加入背包:那么本次的
dp[i][j]
就为dp[i-i0][j-j0] + 1
- 如果当前元素不加入背包:那么本次的
dp[i][j]
和上一轮的就想等,所以,判断两个哪个大即可。
初始化全初始化为0就OK。
- 一维dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
12. 518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change-2
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int change(int amount, vector<int>& coins) {
//表示背包大小为dp[i]的时候的方案数
vector<int> dp(amount+1,0);
// 当背包大小为0的时候有1种方案
dp[0] = 1;
for(int i = 0; i < coins.size(); i++){
for(int j = coins[i]; j <= amount; j++){
dp[j] +=dp[j - coins[i]];
}
}
return dp[amount];
}
};518. 零钱兑换 II
由于每个物品都有无限个,所以这个是一个完全背包问题。和01背包的区别就是j层的遍历顺序相反。
然后比较重要的是内外层循环的遍历次序: (求组合数和求排列数)
我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
代码如下:
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
假设:coins[0] = 1,coins[1] = 5。
那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序,代码如下:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时dp[j]里算出来的就是排列数!
13.377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-iv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0] = 1;
for(int i = 0; i <= target; i++){ // 容量循环放外层
for(int j = 0; j < nums.size(); j++){ // 物品循环放内层
if(i >= nums[j]&& dp[i] < INT_MAX - dp[i - nums[j]]){
dp[i]+=dp[i-nums[j]];
}
}
}
return dp[target];
}
};
和上题一样,不过组合数变成了排列数。
14.322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if(amount == 0) return amount;
vector<int> dp(amount+1,1001); // 最多不超过1000个硬币,按照题设条件
dp[0] = 0;
for(int i = 0; i < coins.size(); i++){
for(int j = coins[i]; j <=amount; j++){
dp[j] = min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount] == 1001) return -1;
return dp[amount];
}
};
本题个人认为最难的地方应该是初始化,和之前的题目不一样,不能全部初始化为0,而要初始化为一个较大的值,防止后面的计算结果被初始值给覆盖。而dp[0]又必须初始化为0。不然结果依旧出不来,这里是我觉得比较奇怪的一个地方。
- dp数组如何初始化
首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;
其他下标对应的数值呢?
考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
所以下标非0的元素都是应该是最大值。
15. 279.完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int numSquares(int n) {
// 和上一题差不多的意思,完全背包,组合问题,但是元素的值需要自己搞
// 存的值为 0 1 2 3 4 5 ... 100
vector<int> nums(101,0);
for(int i = 1; i <= 100; i++){
nums[i] = i*i;
}
// 由于又是最小的数量,所以按照惯例还是初始化为最大的整数值
vector<int> dp(n+1,INT_MAX);
dp[0] = 0;
for(int i = 1; i < nums.size(); i++){
for(int j = nums[i]; j <= n; j++){
dp[j] = min(dp[j],dp[j-nums[i]]+1);
}
}
return dp[n];
}
};279.完全平方数
本题和上题差不多,只是coins数组需要自己构造。
从集合中选元素,均可以转化为背包问题。
16. 139.单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-break
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) { // 遍历背包
for (int j = 0; j < i; j++) { // 遍历物品
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end() && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
这题也可以转化成背包问题,只是有一些另类的感觉
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
17. 198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1) return nums[0];
// dp[i]为扒到第i间房获得的最高金额
// dp[i] = max(dp[i-1],dp[i-2]+nums[i])。偷 or 不偷 这是个问题
// 初始化
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.size()-1];
}
};
dp[i]为扒到第i间房获得的最高金额
dp[i] = max(dp[i-1],dp[i-2]+nums[i])。偷 or 不偷 这是个问题
初始化前俩就行。
18. 213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1) return nums[0];
// 个人认为分两种情况讨论
// 一种是不偷第一间房,那么后面的就是最大的
// 一种是偷第一间房,那么出去最后的一间房就是最大的
vector<int> n1(nums.begin(),nums.end()-1);
vector<int> n2(nums.begin()+1,nums.end());
return max(rob1(n1),rob1(n2));
}
int rob1(vector<int>& nums) {
if(nums.size()==1) return nums[0];
// dp[i]为扒到第i间房获得的最高金额
// dp[i] = max(dp[i-1],dp[i-2]+nums[i])。偷 or 不偷 这是个问题
// 初始化
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.size()-1];
}
};
这题和上题不同之处在于房子围成了环形,但是可以单独拎出来讨论,分为不偷第一间房和偷第一间房。然后再和之前一样做就可以了
19.337.打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber-iii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int rob(TreeNode* root) {
// 由于对于一根节点二叶节点的基础二叉树来说,偷根节点和偷二叶节点取决于谁大,放在一起就是哪一层更大
// 所以先进行层次遍历,然后再使用dp求解
vector<int> nums;
queue<TreeNode*> que;
que.push(root);
while(!que.empty()){
int count = que.size();
int sum = 0; // 记录每一层的总和
for(int i = 0; i < count; i++){
TreeNode* tem = que.front();
que.pop();
if(tem->left!=nullptr) que.push(tem->left);
if(tem->right!=nullptr) que.push(tem->right);
sum+=tem->val;
}
nums.push_back(sum);
}
for(auto it : nums){
cout<<it<<" ";
}
cout<<endl;
return rob1(nums);
}
int rob1(vector<int>& nums) {
if(nums.size()==1) return nums[0];
// dp[i]为扒到第i间房获得的最高金额
// dp[i] = max(dp[i-1],dp[i-2]+nums[i])。偷 or 不偷 这是个问题
// 初始化
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
}
for(auto it : dp){
cout<<it<<" ";
}
return dp[nums.size()-1];
}
};
class Solution {
public:
unordered_map<TreeNode* , int> umap; // 记录计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
umap[root] = max(val1, val2); // umap记录一下结果
return max(val1, val2);
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 这里由于用的是二维的dp所以就是表示的偷or不偷 不是考虑偷or不偷
// dp[0] 表示不偷当前节点所能获得的最大钱财
// dp[1] 表示偷当前节点所能获得的最大钱财
vector<int> robTree(TreeNode* root){
vector<int> dp(2);
// 终止条件 节点位null 偷不偷都是0
if(root==nullptr) return vector<int>{0,0};
// 叶节点不偷为0,偷为叶节点的值
if(root->left == nullptr && root->right==nullptr){
return vector<int>{0,root->val};
}
// 分别获得每棵子树的偷 or 不偷的最大钱数
vector<int> left = robTree(root->left);
vector<int> right = robTree(root->right);
// 不偷当前节点的情况
dp[0] = max(left[0],left[1]) + max(right[0],right[1]);
// 偷当前节点的情况
dp[1] = left[0]+right[0]+root->val;
return dp;
}
int rob(TreeNode* root) {
// 在树上进行的动态规划操作
vector<int> res = robTree(root);
return max(res[0],res[1]);
}
};
看起来可以,但是行不通,需要另寻他法
第二种是采用回溯,单纯暴力回溯会超时,但是加上记忆化回溯就不会超时了。
第三种是动态规划的方法,是树上的动态规划,由于使用DFS进行递归遍历,所以每一棵树返回的dp都是表示dp[0]偷、dp[1]不偷两种状态下所能获得的最大金额,和之前的不一样,之前的题目是考虑偷or不偷,这里是直接偷和不偷。
先算出两颗子树所能获得的最大金额,再算出本棵树偷or不偷所能获得的最大金额,最后递归返回到根节点求解。
20. 121. 买卖股票的最佳时机
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
return dp[len - 1][1];
}
};
这题强行用动态规划的思路还是比较麻烦的。
可以直接记录一个i前最小值和i值的差值,找出最大的那一个就OK了。
21. 122.买卖股票的最佳时机II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 试试用动态规划做
// 这里只能卖一次
vector<int> dp(prices.size());
int his = prices[0];
dp[0] = 0;
for(int i = 1; i < prices.size(); i++){
dp[i] = max(prices[i]-his+dp[i-1],dp[i-1]);
his = prices[i];
}
return dp[prices.size()-1];
}
};
dp[i]表示第i天所能获得的最大利润,再用一个his记录上一天的价格,状态转移方程是两种状态的比较,看哪一种比较大,第一种是第i天按兵不动,所以就和dp[i-1]一样大,第二种状态是卖出股票,那么就是prices[i]-his+dp[i-1]。其实这中做法就像是贪心转过来的,如果比上一天大就直接卖,不大就不卖。其实第一种做法就是贪心做法。算不上动态规划。
22. 300.最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// dp[i]指的是i结尾的子序列的最大长度
int result = 1;
vector<int> dp(nums.size(),1);
for(int i = 1; i < nums.size(); i++){
for(int j = 0; j < i; j++){
if(nums[j]<nums[i]){
dp[i] = max(dp[i],dp[j]+1);
result = max(result,dp[i]);
}
}
}
return result;
}
};
由于每个dp[i]的值都和i之前的dp值相关,所以每次都遍历一遍。
24. 674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
// dp[i] 表示i结尾的最长连续递增子序列
vector<int> dp(nums.size(),1);
int result = 1;
for(int i = 1; i < nums.size(); i++){
if(nums[i]>nums[i-1]){
dp[i] = dp[i-1]+1;
}
if(dp[i]>result) result = dp[i];
}
return result;
}
};674. 最长连续递增序列
本题的dp[i]就只和dp[i-1]有关了。比上面那题其实更简单一些。
25. 718. 最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度
。
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size(),vector<int>(nums2.size(),0));
// init
int flag = 0;
for(int i = 0; i < nums1.size(); i++){
if(nums1[i]==nums2[0]){
dp[i][0] = 1;
flag = 1;
}
}
for(int j = 0; j < nums2.size(); j++){
if(nums1[0]==nums2[j]){
dp[0][j] = 1;
flag = 1;
}
}
int result = flag;
for(int i = 1; i < nums1.size(); i++){
for(int j = 1; j < nums2.size(); j++){
if(nums1[i]==nums2[j]){
dp[i][j] = dp[i-1][j-1] +1;
}
if( dp[i][j]> result) result = dp[i][j];
}
}
return result;
}
};
dp[i][j]
表示以i,j结尾的最长子数组长度。
状态转移方程:dp[i][j] = dp[i-1][j-1] +1;
当i,j对应的值相等时转移,否则保持原样就好。
然后可以进一步化简为滚动数组的形式。
26. 1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// dp[i][j]表示以i,j结尾的公共序列的最大长度
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m,vector<int>(n,0));
// init
bool flag = false;
for(int i = 0; i < m; i++){
if(text1[i]==text2[0]){
flag = true;
}
if(flag){
dp[i][0] = 1;
}
}
flag = false;
for(int j = 0; j < n; j++){
if(text1[0] == text2[j]){
flag = true;
}
if(flag){
dp[0][j] = 1;
}
}
// process
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(text1[i] == text2[j]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m-1][n-1];
}
};
本题和上题的不同之处在于可以不连续,所以当i和j两个元素不相等的时候可以取左边和上面的最大值。递推的时候绕进去了,当相等的时候就应该看dp[i-1][j-1]
这样dp[i][j] = dp[i-1][j-1]+1;
才ok。
27. 1035.不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/uncrossed-lines
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
vector<vector<int>> dp(m,vector<int>(n,0));
// init
bool flag = false;
for(int i = 0; i < m; i++){
if(nums1[i]==nums2[0]){
flag = true;
}
if(flag){
dp[i][0] = 1;
}
}
flag = false;
for(int j = 0; j < n; j++){
if(nums1[0]==nums2[j]){
flag = true;
}
if(flag){
dp[0][j] = 1;
}
}
// process
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(nums1[i]==nums2[j]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m-1][n-1];
}
};1035.不相交的线
这题的思路基本和上题的一样,连状态转移方程都一样。
绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
28. 53. 最大子序和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// dp[i] 以i元素结尾的子数组的最大和
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
int result = dp[0];
for(int i = 1; i < nums.size(); i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>result) result = dp[i];
}
return result;
}
};
比较简单
29. 392.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
class Solution {
public:
bool isSubsequence(string s, string t) {
// 判断子序列
// dp[i] 表示子序列长度,如果最后满足长度为字符串长度即可。
int i = 0 , j = 0;
while(i<s.size() && j < t.size()){
if(s[i]==t[j]){
i++;
j++;
}else{
j++;
}
}
return i==s.size();
}
};
第一种做法,双指针,还是比较简单的。O(s.size()+t.size())
第二种可以用dp判断最长公共子序列长度,但感觉稍微有些麻烦 复杂度O(n^2)
30.115.不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/distinct-subsequences
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) {
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()];
}
};
class Solution {
public:
int numDistinct(string s, string t) {
// dp[i][j] 表示以i结尾和以j结尾的两个字符串t'在s'中出现的次数
vector<vector<unsigned long long>> dp(s.size(),vector<unsigned long long>(t.size(),0));
// init 初始化错了
// 空字符串出现的个数为1,全删了就可以得到空字符串了
int count = 0;
for(int i = 0; i < s.size(); i++){
if(s[i]==t[0]){
count++;
}
dp[i][0] = count;
}
// 这里的初始化没有问题
// 当j等于1的时候t的长度就至少为2,不可能出现在s中
for(int j = 1; j < t.size(); j++){
dp[0][j] = 0;
}
// process
for(int i = 1; i < s.size(); i++){
for(int j = 1; j <t.size(); j++){
if(s[i]==t[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()-1][t.size()-1];
}
};
这题还是比较难的,明天自己复现一下。dp[i][j]
:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
- s[i - 1] 与 t[j - 1]相等
- s[i - 1] 与 t[j - 1] 不相等
当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。
一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
第二种是自己的风格,直接将i,j定义成dp[i][j]
:以i为结尾的s子序列中出现以j为结尾的t的个数为dp[i][j]。就是初始化的时候会显得稍微麻烦一点。
31.583. 两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
**相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
class Solution {
public:
int minDistance(string word1, string word2) {
// dp[i][j] 表示i-1结尾的字符串和j-1结尾的字符串变为相同所需要的最小步数
// 这里是直接版本,回头来个间接版本的dp,间接版本的dp的初始化会更简单一些。
int m = word1.size();
int n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
// init
for(int i = 0; i <= m; i++){
dp[i][0] = i;
}
for(int j = 1; j <= n; j++){
dp[0][j] = j;
}
for(int i = 1; i <=m; i++){
for(int j = 1; j<=n; 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],dp[i][j-1])+1;
}
}
}
// for(auto it:dp){
// for(auto itt :it){
// cout<<itt<<" ";
// }
// cout<<endl;
// }
return dp[m][n];
}
};583. 两个字符串的删除操作
这题有体现出dp[i][j]
:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。初始化的优势了,如果还是i和j的话,那么初始化还是挺复杂的。
- 确定递推公式
- 当word1[i - 1] 与 word2[j - 1]相同的时候
- 当word1[i - 1] 与 word2[j - 1]不相同的时候
当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
情况一:删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
那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
但是好像这里考虑前两种情况就能够AC了。
32. 72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
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;
}
}
}
return dp[word1.size()][word2.size()];
}
};
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
if (word1[i - 1] == word2[j - 1])
不操作
if (word1[i - 1] != word2[j - 1])
增
删
换
也就是如上4种情况。
if (word1[i - 1] == word2[j - 1])
那么说明不用任何编辑,dp[i][j]
就应该是 dp[i - 1][j - 1]
,即dp[i][j] = dp[i - 1][j - 1];
此时可能有同学有点不明白,为啥要即dp[i][j] = dp[i - 1][j - 1]
呢?
那么就在回顾上面讲过的dp[i][j]
的定义,word1[i - 1]
与 word2[j - 1]
相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2
的最近编辑距离dp[i - 1][j - 1]
就是 dp[i][j]
了。
在下面的讲解中,如果哪里看不懂,就回想一下dp[i][j]
的定义,就明白了。
在整个动规的过程中,最为关键就是正确理解dp[i][j]
的定义!
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"
, 最终的操作数是一样
操作三:替换元素,word1
替换word1[i - 1]
,使其与word2[j - 1]
相同,此时不用增加元素,那么以下标i-2
为结尾的word1
与 j-2
为结尾的word2
的最近编辑距离 加上一个替换元素的操作。
即 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;