Leetcode刷题笔记——动态规划之子序列问题篇

Leetcode刷题笔记------动态规划之子序列问题篇

一、回文

第一题:回文子串

Leetcode647. 回文子串:中等题 (详情点击链接见原题)

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串

1. 确定 dp 数组(dp table)以及下标的含义

在定义 dp 数组的时候 很自然就会想题目求什么,我们就如何定义 dp 数组

布尔类型的dp[i][j]:表示区间范围 [i,j] 的子串是否为回文子串,如果是则为True

2. 确定递推公式

s[i]s[j] 不相等,dp[i][j] 一定是 false

s[i]s[j] 相等时,有如下三种情况
case1:下标 i 与下标 j 相同,同一个字符当然是回文子串
case2:下标 ij 相差为1 ,如aa的时候也是回文子串
case3:下标ij大于1的时候,例如cabac,此时s[i] == s[j],判定区间[i,j]是不是回文子串就看[i + 1, j - 1]是不是回文(为True)就可以了

3. dp 数组如何初始化
dp[i][j] 初始化为 false,因为不可能一开始就全匹配上

4. 确定遍历顺序

这道题的遍历顺序有点讲究,如果按照我们的惯性思维从上到下,从左到右取遍历,那么就得不出结果

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

cbabc为例,对应的 dp 数组为

python代码解法

python 复制代码
class Solution:
    def countSubstrings(self, s: str) -> int:
        dp = [[False] * len(s) for _ in range(len(s))]
        result = 0  # result 用来保存回文子串的数目
        for i in range(len(s) - 1, -1, -1):  # 从下到上
            for j in range(i, len(s)):
                if s[i] == s[j]:
                    if j - i <= 1:
                        result += 1
                        dp[i][j] = True
                    elif dp[i + 1][j - 1]:
                        result += 1
                        dp[i][j] = True
        return result

第二题:最长回文子串

Leetcode5:最长回文子串:中等题 (详情点击链接见原题)

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串

python代码解法

python 复制代码
class Solution:
    def longestPalindrome(self, s: str) -> str:
        dp = [[False] * len(s) for _ in range(len(s))]
        max_len = 0  # result 用来保存回文子串的数目
        result = ""
        for i in range(len(s) - 1, -1, -1):  # 从下到上
            for j in range(i, len(s)):
                if s[i] == s[j]:
                    if j - i <= 1 or dp[i + 1][j - 1]:
                        dp[i][j] = True
                    if dp[i][j] and j - i + 1 > max_len:
                        max_len = j - i + 1
                        result = s[i: i + max_len]
        return result

二、子序列(连续)

第一题:最长重复子数组

Leetcode718. 最长重复子数组:中等题 (详情点击链接见原题)

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

解题思路:子数组其实就是连续子序列

1. 确定 dp 数组(dp table)以及下标的含义
dp[i][j]:以下标 i - 1 为结尾的 A 和以下标 j - 1 为结尾的 B,最长重复子数组的长度为 dp[i][j]dp[i][j] 的定义决定了我们在遍历 dp[i][j]的时候 ij 都要从 1 开始】

以 A=[1, 2, 3, 2, 1],B = [3, 2, 1, 4, 7]为例,递推过程如下图所示:
dp[4][2] = 2 的含义为以 下标3为结尾的 A数组 与下标 1 为结尾的 B 数组 的最长重复子数组的长度为 2

2. 确定递推公式
dp[i][j] 的状态只能由 dp[i - 1][j - 1] 推导出来,即当 A[i - 1]B[i - 1] 相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1

3. dp数组如何初始化

根据 dp[i][j] 的定义,dp[i][0]dp[0][j] 其实都是没有意义的【可以看成是以 i - 1 为结尾的 A 和空数组 B 的的最长重复子数组】, 故 dp[i][0]dp[0][j] 初始化为 0

4. 确定遍历顺序

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

python代码解法

python 复制代码
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        n, m = len(nums1), len(nums2)
        result = 0
        dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
        for i in range(1, n + 1):  # 外层循环遍历 nums1
            for j in range(1, m + 1):  # 内层循环遍历 nums2
                if nums1[i - 1] == nums2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                result = max(result, dp[i][j])
        # for i in dp:    # 打印 dp 数组
        #     print(i)
        return result

第二题:最长连续递增序列

Leetcode674. 最长连续递增序列:简单题 (详情点击链接见原题)

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

解题思路:
1. 确定 dp 数组(dp table)以及下标的含义
dp[i]: 以下标 i 为结尾的连续递增的子序列长度为 dp[i](注意这里说以下标 i 为结尾,并没说一定以下标 0 为起始位置)

2. 确定递推公式

如果 nums[i] > nums[i -1],那么以 i 为结尾的连续递增的子序列长度一定等于 以 i - 1 为结尾的连续递增的子序列长度 + 1
递推公式: dp[i] = dp[i - 1] + 1

本题要求的是连续递增子序列,所以只需要比较 nums[i]nums[i - 1],而不用去比较 nums[j]nums[i]j0i 之间遍历】

3. dp数组如何初始化

以下标 i 为结尾的连续递增的子序列长度最少也应该是 1,即 nums[i] 这一个元素

4. 确定遍历顺序

从递推公式上可以看出,dp[i ] 依赖 dp[i - 1],所以一定是从前向后遍历

python代码解法(dp思路)

python 复制代码
class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        dp = [1] * len(nums)
        for i in range(1, len(nums)):
            if nums[i] > nums[i - 1]:
                dp[i] = dp[i - 1] + 1
        return max(dp)

python代码解法(滑窗思路)

python 复制代码
class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        left, right = 0, 1
        ans = 1
        while right < len(nums):
            if nums[right] <= nums[right - 1]:
                left = right
            ans = max(ans, right - left + 1)
            right += 1
        return ans

第三题:最大子数组和

Leetcode53. 最大子数组和:中等题 (详情点击链接见原题)

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

1. 确定 dp 数组(dp table)以及下标的含义
dp[i]:包括下标 i (以 nums[i] 为结尾)的最大连续子序列和为 dp[i]

2. 确定递推公式
dp[i] 只有两个方向可以推导出来:因为 dp[i - 1] < 0 的话会拉低连续子序列的和,如果拉低还不如直接从当前 nums[i] 开始算)
dp[i - 1] + nums[i],即加入 nums[i] 后的连续子序列和(
nums[i]:从头开始计算当前连续子序列和

3. dp数组如何初始化
dp[0] = nums[0]

4. 确定遍历顺序

递推公式中 dp[i] 依赖于 dp[i - 1] 的状态,需要从前向后遍历

注:我们要找最大的连续子序列,就应该找每一个 i 为终点的连续最大子序列

python代码解法

python 复制代码
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = [0] * len(nums)
        dp[0] = nums[0]
        for i in range(1, len(nums)):
            dp[i] = max(dp[i - 1] + nums[i], nums[i])
        # print(dp)
        return max(dp)

三、子序列(不连续)

第一题: 最长递增子序列

Leetcode300. 最长递增子序列:中等题 (详情点击链接见原题)

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序

解题思路:

相对于 Leetcode674. 最长连续递增序列 这一题,本题最大的区别在于不连续

1. 确定 dp 数组(dp table)以及下标的含义

dp[i]:表示 i 之前包括 i 的以 nums[i] 结尾的最长递增子序列的长度

2. 确定递推公式

位置 i 的最长升序子序列等于 j0i - 1各个位置的最长升序子序列 + 1 的最大值

python 复制代码
if nums[i] > nums[j]:
	dp[i] = max(dp[i], dp[j + 1])  # 注意这里不是要 dp[i] 与 dp[j] + 1 进行比较, 而是取dp[j] + 1的最大值

3. dp 数组如何初始化

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

4. 确定遍历顺序
dp[i] 是有0i - 1各个位置的最长递增子序列 推导而来,那么遍历 i 一定是从前向后遍历
j 其实就是遍历 0i - 1, 那么是从前到后还是从后到前都可以

python代码解法

python 复制代码
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        dp = [1] * len(nums)
        for i in range(1, len(nums)):
            for j in range(0, i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        # print(dp)
        return max(dp)

第二题:最长公共子序列

Leetcode1143:最长公共子序列:中等题 (详情点击链接见原题)

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

1. 确定 dp 数组(dp table)以及下标的含义
dp[i][j]:长度为 [0, i - 1] 的字符串 text1 与长度为 [0, j - 1] 的字符串 text2 的最长公共子序列为 dp[i][j]

2. 确定递推公式

3. dp 数组如何初始化
dp[i][0]text1[0, i - 1] 和空串的最长公共子序列是0,dp[i][0] = 0,同理 dp[0][j] = 0

4. 确定遍历顺序
从前向后,从上到下来遍历

python代码解法

python 复制代码
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n, m = len(text1), len(text2)
        dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if text1[i - 1] == text2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    # 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])
        return dp[n][m]

第三题:不相交的线

Leetcode1035. 不相交的线:中等题 (详情点击链接见原题)

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数

四、编辑距离

第一题:判断子序列

Leetcode392. 判断子序列:简单题 (详情点击链接见原题)

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

解题思路
1. 确定 dp 数组(dp table)以及下标的含义
dp[i][j]:长度为 [0, i - 1] 的字符串 s 与长度为 [0, j - 1] 的字符串 t 的相同子序列的长度为 dp[i][j]

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

2. 确定递推公式

python 复制代码
if s[i - 1] == t[i - 1]:    # t中找到一个字符在s中也出现了
	dp[i][j] = dp[i - 1][j - 1] + 1
if s[i - 1] != t[i - 1]:    # 相当于 t 要删除元素,继续匹配
	dp[i][j] = dp[i][j - 1]

3. dp 数组如何初始化
dp[i][j] 是依赖于 dp[i - 1][j - 1] 的,所以 dp[0][0]dp[i][0] 是一定要初始化的

4. 确定遍历顺序
dp[i][j] 都是依赖于 dp[i - 1][j - 1]dp[i][j - 1],所以应该从前向后,从上到下来遍历

5. 举例推导dp数组

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

python代码解法

python 复制代码
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
        for i in range(1, len(s) + 1):
            for j in range(1, len(t) + 1):
                if s[i - 1] == t[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = dp[i][j - 1]
        return True if dp[len(s)][len(t)] == len(s) else False

第二题: 不同的子序列

Leetcode115. 不同的子序列:困难题 (详情点击链接见原题)

给你两个字符串 st ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 10的9次方 + 7 取模

解题思路:

本题相对于编辑距离还是比较简单的,因为本题只有删除操作,本题求的是 s 里面有多少个像 t 这样的子序列,其实就是问这个 s 字符串中有多少种删除元素的方式使得 s 可以变成 t

1. 确定 dp 数组(dp table)以及下标的含义
dp[i][j]:i - 1 为结尾的 s 子序列种出现以 j - 1 为结尾的 t个数dp[i][j]

2. 确定递推公式

case1: s[i - 1] 与 t[j - 1] 相等
s:bagg t: bag
s[3]t[2] 是相同的,但是字符串 s 也可以不用 s[3] 来匹配,s[0]s[1]s[2]s[0]s[1]s[3] 组成的 bag
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]

不需要考虑 st 的最后一位字母,只需要用 dp[i - 1][j - 1]

case2: s[i - 1]t[j - 1] 不相等

s[i - 1]t[j - 1]不相等时,dp[i][j] 只有一部分组成,不用 s[i - 1] 来匹配

3. dp 数组如何初始化

由递推公式可知: 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]: 以下标 i - 1为结尾的 s 删除所有元素,出现空串t【即一种删除所有元素的方式】
dp[0][j]: 空串 s 无论怎么都变成不了 t,所以 dp[0][j] = 0
dp[0][0] = 1: 空字符串 s 可以删除 0 个元素变成空字符串 t

4. 确定遍历顺序

5. 举例推导dp数组

python代码解法

python 复制代码
class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
        for i in range(len(s)):
            dp[i][0] = 1
        for j in range(1, len(t)):
            dp[0][j] = 0
        for i in range(1, len(s) + 1):
            for j in range(1, len(t) + 1):
                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[len(s)][len(t)]

第三题:两个字符串的删除操作

Leetcode583. 两个字符串的删除操作:中等题 (详情点击链接见原题)

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

解题思路

相对于上一题而言,其实就是两个字符串都可以删了
1. 确定 dp 数组(dp table)以及下标的含义
dp[i][j] : 以 i-1 为结尾的字符串 word1,和以 j-1 位结尾的字符串 word2,想要达到相等,所需要删除元素的最少次数

2. 确定递推公式

  • word1[i - 1]word2[j - 1] 相同的时候【不用删除元素】dp[i][j] = dp[i - 1][j - 1]
  • word1[i - 1]word2[j - 1]不相同的时候:dp=min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)
    • case1:删 word1[i - 1],最少操作次数为 dp[i - 1][j] + 1
    • case2: 删 word2[j - 1],最少操作次数为 dp[i][j - 1] + 1
    • case3:同时删 word1[i - 1]word2[j - 1],最少操作次数为 dp[i - 1][j - 1] + 2

3. dp 数组如何初始化

从递推公式中,可以看出来,dp[i][0]dp[0][j] 是一定要初始化的
dp[i][0]word2 为空字符串,以 i-1 为结尾的字符串 word1 要删除多少个元素,才能和 word2 相同呢,很明显 dp[i][0] = i, dp[0][j] = j

4. 确定遍历顺序

从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和 dp[i][j] = dp[i - 1][j - 1] 可以看出 dp[i][j] 都是根据左上方、正上方、正左方推出来的

5.举例推导DP数组

word1:sea,word2:eat 为例

python代码解法

python 复制代码
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        dp = [[0 for _ in range(len(word2) + 1)] for _ in range(len(word1) + 1)]
        for i in range(len(word1) + 1):
            dp[i][0] = i
        for j in range(len(word2) + 1):
            dp[0][j] = j

        for i in range(1, len(word1) + 1):
            for j in range(1, len(word2) + 1):
                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[len(word1)][len(word2)]

第四题:编辑距离

Leetcode72. 编辑距离:中等题 (详情点击链接见原题)

给你两个单词 word1word2, 请返回将 word1 转换成 word2 所使用的最少操作数`

1. 确定 dp 数组(dp table)以及下标的含义

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

2. 确定递推公式
if word1[i - 1] != word2[j - 1]
操作1 : word1 删除一个元素,那么就是以下标 i - 2 为结尾的 word1j-1 为结尾的 word2 的最近编辑距离 再加上一个操作dp[i][j] = dp[i - 1][j] + 1
操作2word2 删除一个元素,那么就是以下标 i - 1 为结尾的 word1j-2 为结尾的 word2 的最近编辑距离 再加上一个操作,dp[i][j] = dp[i][j - 1] + 1

添加元素怎么操作呢?word2 添加一个元素,相当于 word1 删除一个元素

比如 word1 = 'ad', word2 = 'a'word1 删除元素 dword1 = 'a', word2 = 'a'【操作一次】

word2 添加一个元素 dword1 = 'ad', word2 = 'ad'【操作一次】

操作3 :替换元素
word1 替换 word1[i - 1] 使其与 word2[j - 1] 相同,此时不用增删元素,只需一次替换操作就可以让 word1[i - 1]word2[j - 1], dp[i][j] = dp[i - 1][j - 1] + 1

python 复制代码
if word1[i - 1] == word2[j - 1]:  # 既然两个元素相同
	dp[i][j] = dp[i - 1][j - 1]   # 考虑以 i - 2 为下标结尾的 word1 和以 j - 2为下标结尾的 word2 的最近的编辑距离
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

3. dp 数组如何初始化
dp[i][0] :以下标 i-1 为结尾的字符串 word1,和空字符串 word2,最近编辑距离为 dp[i][0]dp[i][0] = i 即对 word1 里面的元素全部都做删除操作

4. 确定遍历顺序

  • dp[i][j] = dp[i - 1][j - 1]
  • dp[i][j] = dp[i - 1][j - 1] + 1
  • dp[i][j] = dp[i][j - 1] + 1
  • dp[i][j] = dp[i - 1][j] + 1
    可以看出 dp[i][j] 是依赖左方,上方和左上方元素的,所以 dp 矩阵中一定是从左到右从上到下去遍历

python代码解法

python 复制代码
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        dp = [[0 for _ in range(len(word2) + 1)] for _ in range(len(word1) + 1)]
        for i in range(0, len(word1) + 1):
            dp[i][0] = i
        for j in range(0, len(word2) + 1):
            dp[0][j] = j
        for i in range(1, len(word1) + 1):
            for j in range(1, len(word2) + 1):
                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[len(word1)][len(word2)]
相关推荐
汇能感知3 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun4 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao4 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾4 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT5 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
ST.J5 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin5 小时前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
小憩-6 小时前
【机器学习】吴恩达机器学习笔记
人工智能·笔记·机器学习
UQI-LIUWJ7 小时前
unsloth笔记:运行&微调 gemma
人工智能·笔记·深度学习
googleccsdn7 小时前
ESNP LAB 笔记:配置MPLS(Part4)
网络·笔记·网络协议