代码随想录训练营 Day44打卡 动态规划 part11
一、力扣1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
-
确定 dp 数组的定义
dp[i][j] 表示 text1 的前 i 个字符(即 text1[0:i-1])和 text2 的前 j 个字符(即 text2[0:j-1])的最长公共子序列的长度。 -
状态转移方程
当 text1[i-1] == text2[j-1] 时,表示当前字符匹配,此时 dp[i][j] = dp[i-1][j-1] + 1。
解释:dp[i-1][j-1] 表示不包括当前字符的最长公共子序列长度,匹配到当前字符后,加上当前这个匹配字符的长度 1。
当 text1[i-1] != text2[j-1] 时,表示当前字符不匹配,此时 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
解释:选择不匹配时,dp[i][j] 由 text1 向前一步或 text2 向前一步的最长公共子序列长度的最大值决定。 -
dp 数组的初始化
当 i = 0 或 j = 0 时,即 text1 或 text2 的前缀为空串时,最长公共子序列的长度为 0,即 dp[i][0] = 0 和 dp[0][j] = 0。 -
遍历顺序
由于 dp[i][j] 依赖于 dp[i-1][j-1]、dp[i-1][j] 和 dp[i][j-1],所以遍历时需要从左到右,从上到下进行。 -
最终结果
最终 dp[len(text1)][len(text2)] 即为所求的最长公共子序列的长度。
以输入:text1 = “abcde”, text2 = “ace” 为例,dp状态如图:
代码实现
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
# 创建一个二维数组 dp,用于存储最长公共子序列的长度
dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)]
# 遍历 text1 和 text2,填充 dp 数组
for i in range(1, len(text1) + 1):
for j in range(1, len(text2) + 1):
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] 不相等,则当前位置的最长公共子序列长度为上方或左方的较大值
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 返回最长公共子序列的长度
return dp[len(text1)][len(text2)]
这段代码实现了通过动态规划来求解两个字符串的最长公共子序列的长度问题。它的时间复杂度为 O(m * n),其中 m 和 n 分别为 text1 和 text2 的长度。
二、力扣1035. 不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 :
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
问题转化
实际上,这道题的核心是求两个数组的最长公共子序列(LCS)的长度,因为最长公共子序列的长度恰好对应可以绘制的不相交线的数量。
那么本题就和我们刚刚讲过的这道题目动态规划:1143.最长公共子序列就是一样的了。
代码实现
class Solution:
def maxUncrossedLines(self, A: List[int], B: List[int]) -> int:
# 创建一个二维数组 dp,用于存储最长公共子序列的长度
dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)]
# 遍历 A 和 B,填充 dp 数组
for i in range(1, len(A) + 1):
for j in range(1, len(B) + 1):
if A[i - 1] == B[j - 1]:
# 如果 A[i-1] 和 B[j-1] 相等,则当前位置的最长公共子序列长度为左上角位置的值加一
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# 如果 A[i-1] 和 B[j-1] 不相等,则当前位置的最长公共子序列长度为上方或左方的较大值
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 返回最长公共子序列的长度,即最大的不相交线的数量
return dp[-1][-1]
三、力扣53. 最大子序和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
-
确定 dp 数组的定义
dp[i] 表示以 nums[i] 结尾的最大连续子数组和。 -
状态转移方程
对于每一个 i,我们有两个选择:
继承前面的子数组和:将 nums[i] 加入之前的子数组,即 dp[i-1] + nums[i]。
重新开始:从 nums[i] 开始,抛弃之前的子数组,即 nums[i]。
状态转移方程:dp[i] = max( dp[i-1] + nums[i] , nums[i] )
-
dp 数组的初始化
dp[0] 表示第一个元素本身的值,因为没有其他元素可以与之组合。
所以,dp[0] = nums[0]。 -
确定遍历顺序
由于 dp[i] 依赖于 dp[i-1] 的状态,所以遍历顺序是从左到右,从 i = 1 开始,直到 len(nums) - 1。 -
最终结果
整个数组的最大子序和可能出现在 dp 数组的任何位置,所以我们需要遍历 dp 数组,找到最大的值。
以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下:
代码实现
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 初始化 dp 数组,dp[i] 表示以 nums[i] 结尾的最大子数组和
dp = [0] * len(nums)
dp[0] = nums[0] # dp[0] 的初始化,表示第一个元素的值
result = dp[0] # 用于存储最大子数组和
# 遍历数组,填充 dp 数组
for i in range(1, len(nums)):
# dp[i] 由两种情况决定:要么继承之前的子数组和,要么重新开始一个新的子数组
dp[i] = max(dp[i - 1] + nums[i], nums[i])
# 更新 result 为当前最大子数组和
result = max(result, dp[i])
# 返回最大子数组和
return result
四、力扣392. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
-
确定 dp 数组的定义
dp[i][j] 表示以 s 的前 i 个字符(即 s[0:i-1])和 t 的前 j 个字符(即 t[0:j-1])的最长公共子序列的长度。 -
状态转移方程
当 s[i-1] == t[j-1] 时,表示当前字符匹配,此时 dp[i][j] = dp[i-1][j-1] + 1。
解释:dp[i-1][j-1] 表示不包括当前字符的最长公共子序列长度,匹配到当前字符后,加上当前这个匹配字符的长度 1。
当 s[i-1] != t[j-1] 时,表示当前字符不匹配,此时 dp[i][j] = dp[i][j-1]。
解释:表示我们在字符串 t 中尝试删除当前字符 t[j-1],看看在不包括 t[j-1] 的前提下,s[0:i-1] 和 t[0:j-2] 的最长公共子序列长度是多少。 -
dp 数组的初始化
dp[0][j] = 0:当 s 为空字符串时,最长公共子序列长度为 0,因为空字符串的子序列长度只能是 0。
dp[i][0] = 0:当 t 为空字符串时,最长公共子序列长度也是 0,因为 t 不存在字符与 s 匹配。 -
确定遍历顺序
由于 dp[i][j] 依赖于 dp[i-1][j-1] 和 dp[i][j-1],所以遍历时需要从左到右,从上到下进行。 -
最终结果
最终判断 dp[len(s)][len(t)] 是否等于 len(s),如果相等,则说明 s 是 t 的子序列,否则不是。
代码实现
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
# 创建一个二维数组 dp,用于存储最长公共子序列的长度
dp = [[0] * (len(t) + 1) for _ in range(len(s) + 1)]
# 遍历 s 和 t,填充 dp 数组
for i in range(1, len(s) + 1):
for j in range(1, len(t) + 1):
if s[i - 1] == t[j - 1]:
# 如果 s[i-1] 和 t[j-1] 相等,则当前位置的最长公共子序列长度为左上角位置的值加一
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# 如果 s[i-1] 和 t[j-1] 不相等,则当前位置的最长公共子序列长度为上方的值
dp[i][j] = dp[i][j - 1]
# 判断 dp[len(s)][len(t)] 是否等于 s 的长度
return dp[len(s)][len(t)] == len(s)