动态规划子序列问题集合

目录

300.最长递增子序列

题目描述

解题思路

674.最长连续递增序列

题目描述

解题思路

718. 最长重复子数组

题目描述

解题思路

拓展

1143.最长公共子序列

题目描述

解题思路

1035.不相交的线

题目描述

解题思路

53.最大子序和

题目描述

解题思路

392.判断子序列

题目描述

解题思路

115.不同的子序列

题目描述

解题思路

583.两个字符串的删除操作

题目描述

解题思路

72.编辑距离

题目描述

解题思路

647.回文子串

题目描述

解题思路

516.最长回文子序列

题目描述

解题思路

总结


300.最长递增子序列

题目描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

  • 输入:nums = [10,9,2,5,3,7,101,18]
  • 输出:4
  • 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

  • 输入:nums = [0,1,0,3,2,3]
  • 输出:4

示例 3:

  • 输入:nums = [7,7,7,7,7,7,7]
  • 输出:1

提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 104

解题思路

首先“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”,子序列可以为空。

  1. 确定dp数组以及下标含义

dp[i] 表示以数组 nums 中第 i 个元素(即 nums[i])结尾的最长递增子序列的长度。换句话说,dp[i] 存储的是截止到 nums[i] 时的最优解。

  1. 确定递推公式

位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值

  1. 初始化

每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

  1. 确定遍历顺序

dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。

j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要把 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

  1. 推导dp数组

输入:[0,1,0,3,2],dp数组的变化如下:

0

1

2

3

4

i=1

1

2

1

1

1

i=2

1

2

1

1

1

i=3

1

2

1

3

1

i=4

1

2

1

3

3

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                //从前向后遍历每一个i之前的元素,小于则加入到子序列中
                if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
            }
            if (dp[i] > result) result = dp[i]; // 取长的子序列
        }
        return result;
    }
};

这里返回的是dp数组中的最大值

为什么??

回顾一下dp数组的定义:dp[i] 表示以数组 nums 中第 i 个元素(即 nums[i])结尾的最长递增子序列的长度,即求得的最长递增子序列是以num[i]结尾的

即:整个数组 nums 中的最长递增子序列不一定以 nums[n-1] 结尾

所以返回的不一定是dp[n-1]

674.最长连续递增序列

题目描述

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

  • 输入:nums = [1,3,5,4,7]
  • 输出:3
  • 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:

  • 输入:nums = [2,2,2,2,2]
  • 输出:1
  • 解释:最长连续递增序列是 [2], 长度为1。

提示:

  • 0 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9

解题思路

  1. 确定dp数组以及下标含义

dp[i] ,以 nums[i] 结尾的最长连续递增序列的长度是dp[i]

  1. 确定递推公式

如果nums[i] > nums[i-1],dp[i] = dp[i-1]+1;因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。

  1. 初始化

最短的长度就是nums[i]本身,所以都初始化为1

  1. 确定遍历顺序

从前向后遍历,因为求的是连续子序列,只需要一层for循环即可

  1. 推导dp数组

输入nums = [1,3,5,4,7]为例,dp数组状态如下:

下标0

1

2

3

4

1

2

3

1

2

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        int result = 1;
        vector<int> dp(nums.size() ,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;
    }
};

同理这里返回的是dp数组中的最大值

718. 最长重复子数组

题目描述

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:

  • A: [1,2,3,2,1]
  • B: [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3, 2, 1] 。

提示:

  • 1 <= len(A), len(B) <= 1000
  • 0 <= A[i], B[i] < 100

解题思路

题目中说的子数组,其实就是连续子序列。

  1. 确定dp数组以及下标含义

dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。

  1. 确定递推公式

根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。

即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

根据递推公式可以看出,遍历i 和 j 要从1开始!

当A[i - 1] 和B[j - 1]不相等时,不符合题目条件,就不进行其他操作,也就没有什么其他max取最大值之类的操作了

  1. 初始化

根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!

但dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;

所以dp[i][0] 和dp[0][j]初始化为0。

举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。

  1. 确定遍历顺序

外层for循环遍历A,内层for循环遍历B。

  1. 推导dp数组

A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:

B

3

2

1

4

7

A

0

0

0

0

0

0

1

0

1

2

0

1

3

0

1

2

0

2

1

0

3

没填的dp数组值都是0

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int result = 0;
        for (int i = 1; i <= nums1.size(); i++) {
            for (int j = 1; j <= nums2.size(); j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if (dp[i][j] > result) result = dp[i][j];
            }
        }
        return result;
    }
};

拓展

前面讲了 dp数组为什么定义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。

我就定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。不行么?

当然可以,就是实现起来麻烦一些。

如果定义 dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,那么 第一行和第一列毕竟要进行初始化,如果nums1[i] 与 nums2[0] 相同的话,对应的 dp[i][0]就要初始为1, 因为此时最长重复子数组为1。 nums2[j] 与 nums1[0]相同的话,同理。

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int result = 0;

        // 要对第一行,第一列经行初始化
        for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1;
        for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1;

        for (int i = 0; i < nums1.size(); i++) {
            for (int j = 0; j < nums2.size(); j++) {
                if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if (dp[i][j] > result) result = dp[i][j];
            }
        }
        return result;
    }
};

这种写法 一定要多写一段初始化的过程。

而且为了让 if (dp[i][j] > result) result = dp[i][j]; 收集到全部结果,两层for训练一定从0开始遍历,这样需要加上 && i > 0 && j > 0的判断。

1143.最长公共子序列

题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

  • 输入:text1 = "abcde", text2 = "ace"
  • 输出:3
  • 解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

  • 输入:text1 = "abc", text2 = "abc"
  • 输出:3
  • 解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

  • 输入:text1 = "abc", text2 = "def"
  • 输出:0
  • 解释:两个字符串没有公共子序列,返回 0。

提示:

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000 输入的字符串只含有小写英文字符

解题思路

  1. 确定dp数组以及下标含义

dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

  1. 确定递推公式

text1[i - 1] 与 text2[j - 1]相同时,dp[i][j] = dp[i-1][j-1] +1

text1[i - 1] 与 text2[j - 1]不相同时,那就比较text1[i-2] 与 text2[j - 1],或者text1[i-1] 与 text2[j - 2],取两者的最大值(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]);

  1. 初始化

先看看dp[i][0]应该是多少呢?

test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;

同理dp[0][j]也是0。

其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。

  1. 确定遍历顺序

由递推公式可以得出,从左到右,从上到下遍历

  1. 推导dp数组

以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图

a

c

e

0

0

0

0

a

0

1

1

1

b

0

1

1

1

c

0

1

2

2

d

0

1

2

2

e

0

1

2

3

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for (int i = 1; i <= text1.size(); i++) {
            for (int j = 1; j <= text2.size(); j++) {
                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]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};

对递推公式和初始化不理解,对照dp数组的定义,把dp数组过程推导一遍就能理解了,特别递推公式的理解。

1035.不相交的线

题目描述

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。

以这种方法绘制线条,并返回我们可以绘制的最大连线数。

解题思路

绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!

直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。

本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

53.最大子序和

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

解题思路

  1. 确定dp数组以及下标含义

dp[i]: 以下标i(nums[i] )结尾的最大连续子序列的和为dp[i]

  1. 确定递推公式

dp[i]只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和

一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

  1. 初始化

dp[0] = nums[0],非0下标dp数组可以初始化为0,后面遍历过程中,会被覆盖

  1. 确定遍历顺序

从前向后遍历

  1. 推导dp数组

输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下:

下标0

-2

下标1

1

2

-3

3

4

4

-1

5

2

6

1

7

-5

8

4

-2

1

-2

4

3

5

6

1

5

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        vector<int> dp(nums.size());
        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]; // result 保存dp[i]的最大值
        }
        return result;
    }
};

这里返回的是dp数组中的最大值,因为求得的连续最大子序列不一定是以nums[nums.size() - 1]结尾的。

392.判断子序列

题目描述

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:

  • 输入:s = "abc", t = "ahbgdc"
  • 输出:true

示例 2:

  • 输入:s = "axc", t = "ahbgdc"
  • 输出:false

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4

两个字符串都只由小写字符组成

解题思路

  1. 确定dp数组以及下标含义

dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

  1. 确定递推公式

s[i - 1] 与 t[j - 1] 相等时, dp[i][j] = dp[i-1][j-1] +1

s[i - 1] 与 t[j - 1] 不相等时,dp[i][j]就沿用dp[i][j-1],这题和1143最长公共子序列的递推公式差不多吗,但是本题不会对s进行删除操作

  1. 初始化

从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以dp[0][0]和dp[i][0]是一定要初始化的。

在定义dp[i][j]含义的时候为什么要表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

因为这样的定义在dp二维矩阵中可以留出初始化的区间,如图:

  1. 确定遍历顺序

同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右

  1. 推导dp数组

dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。

注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。

a

h

b

g

d

c

0

0

0

0

0

0

0

a

0

1

1

1

1

1

1

b

0

0

0

2

2

2

2

c

0

0

0

0

0

0

3

class Solution {
public:
    bool isSubsequence(string s, string t) {
        vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 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] + 1;
                else dp[i][j] = dp[i][j - 1];
            }
        }
        if (dp[s.size()][t.size()] == s.size()) return true;
        return false;
    }
};

115.不同的子序列

题目描述

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

提示:

  • 0 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成

解题思路

  1. 确定dp数组以及下标含义

dp[i][j]:表示以下标i-1结束的s子序列 在 以下标j-1结束的t子序列 中出现的个数是dp[i][j]

  1. 确定递推公式

当s[i-1] == t[j-1]时,dp[i][j]可以有两部分组成。

一部分是s[i-1]来匹配,那么就是dp[i-1][j-1]

一部分是用以s[i-2]结尾的子序列匹配,这部分是之前已经计算好的,所以是dp[i-1][j]

例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。

所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

当s[i - 1] 与 t[j - 1]不相等时,就只能用s[i-2]结尾的子序列匹配,即:dp[i - 1][j]

  1. 初始化

从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,那么 dp[i][0] 和dp[0][j]是一定要初始化的。

dp[i][0]=1,把s串的元素都删掉就能匹配为空的t串,

dp[0][j] = 0,s串为空,怎么样都不能凑出t串,所以是0,

dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。

其他dp元素可以初始化为0,在后面计算中会被覆盖掉

  1. 确定遍历顺序

从上到下 从左到右

  1. 推导dp数组

以s:"baegg",t:"bag"为例,推导dp数组状态如下:

b

a

g

1

0

0

0

b

1

1

0

0

a

1

1

1

0

e

1

1

1

0

g

1

1

1

1

g

1

1

1

2

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()];
    }
};

583.两个字符串的删除操作

题目描述

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: "sea", "eat"
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

解题思路

  1. 确定dp数组以及下标含义

dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

  1. 确定递推公式
  • 当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

dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});

因为 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);

这里的删是指不对这个元素操作,直接跳过

  1. 初始化

dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。

dp[0][j]的话同理

  1. 确定遍历顺序

从上到下,从左到右

  1. 推导dp数组

以word1:"sea",word2:"eat"为例

e

a

t

0

1

2

3

s

1

2

3

4

e

2

1

2

3

a

3

2

1

2

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
        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][j - 1] + 1);
                }
            }
        }
        return dp[word1.size()][word2.size()];
    }
};

72.编辑距离

题目描述

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
  • 示例 1:
  • 输入:word1 = "horse", word2 = "ros"
  • 输出:3
  • 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
  • 示例 2:
  • 输入:word1 = "intention", word2 = "execution"
  • 输出:5
  • 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1 和 word2 由小写英文字母组成

解题思路

  1. 确定dp数组以及下标含义

dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。

  1. 确定递推公式

if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][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", 最终的操作数是一样!

  • 操作三:替换元素,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;

  1. 初始化

dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。

那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;

同理dp[0][j] = j;

  1. 确定遍历顺序

从左到右从上到下去遍历。

  1. 推导dp数组

r

o

s

0

1

2

3

h

1

1

2

3

o

2

2

1

2

r

3

2

2

2

s

4

3

3

2

e

5

4

4

3

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()];
    }
};

647.回文子串

题目描述

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:"abc"
  • 输出:3
  • 解释:三个回文子串: "a", "b", "c"

示例 2:

  • 输入:"aaa"
  • 输出:6
  • 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:输入的字符串长度不会超过 1000

解题思路

  1. 确定dp数组以及下标含义

很多这种子序列相关的题目,在定义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。

  1. 确定递推公式

当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。

  1. 初始化

dp[i][j]初始化为false。

  1. 确定遍历顺序

首先从递推公式中可以看出,情况三是根据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]都是经过计算的

  1. 推导dp数组

举例,输入:"aaa",dp[i][j]状态如下:

1

1

1

0

1

1

0

0

1

注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {  // 注意遍历顺序
            for (int j = i; j < s.size(); j++) {
                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;
    }
};

516.最长回文子序列

题目描述

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。

示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。

提示:

  • 1 <= s.length <= 1000
  • s 只包含小写英文字母

解题思路

  1. 确定dp数组以及下标含义

dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]

  1. 确定递推公式

如果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]看看哪一个可以组成最长的回文子序列。

加入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]);

注意不用在max(dp[i + 1][j], dp[i][j - 1])基础上+1

  1. 初始化

当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]才不会被初始值覆盖。

  1. 确定遍历顺序

从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图:

所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的

j的话,可以正常从左向右遍历。

  1. 推导dp数组

输入s:"cbbd" 为例,dp数组状态如图:

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); 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][s.size() - 1];
    }
};

总结

子序列问题一般是问什么,就把dp数组定义为什么

关于两个字符串的子序列问题,dp[i][j]:表示以下标i-1结束的s子序列 在 以下标j-1结束的t子序列 中出现的个数是dp[i][j],这样定义dp数组方便初始化,但是确定递推公式时一定要注意理解dp数组含义

确定遍历顺序时,可以画一个图,例如

动态规划的五步都很重要,当不能理解递推公式时,就把dp数组推导一遍,再结合dp数组定义理解

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值