代码随想录 day47 动态规划part11 最长重复子数组 最长公共子序列 不相交的线 最大子序和

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

思路

A 与 B 的重复, 且位置不能更换

在 A 中, 也在 B 中: 连续的子数组长度。
dp[i][j]: A 中 0~i-1, B 中 0 ~ j-1 的连续数组的长度。

dp[i][j]: 连续数组长度, 所以 i j 来自哪里?--- i j 的上一个是多少 (i-1, j-1), 两个数组两个坐标同时变化
for i in range(0, len(A)):
    for j in range(0, len(B)):
i = 0, j = 0
i = 0, j = 1
i = 0, j = 2

i = 1, j = 1
i    0 1 2 3 4
A    1 2 3 4 5

j    0 1 2 3 4
B    2 3 4 5 1

lens = 1  i = 1 j = 0
lens = 2  i = 2 j = 1
lens = 3  i = 3 j = 2

所以 i,j 从 i-1, j-1 推导而来。 dp[i][j] = dp[i-1][j-1] +1
那么(i, j-1), (i-1,j)表示什么

例如 i = 2, j = 1, (2, 0) (1,0)(2,1)有什么关系吗?
没有

code python

from typing import List
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        len1, len2 = len(nums1), len(nums2)
        dp = [[0 for _ in range(len2+1)] for _ in range(len1+1)]
        res = 0
        # dp的含义要记住
        for i in range(len1):
            for j in range(len2):
                if nums1[i] == nums2[j]:
                    dp[i+1][j+1] = dp[i][j] + 1
                    if res < dp[i+1][j+1]:
                        res = dp[i+1][j+1]
        return res

小结

A, B 是两个数组,所以 i,j 的变化是同时的
i-1,j-1 ----> i,j
i-2,j-1 ----> i-1,j
i-1,j-2 ----> i,j-1

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

dp[i] : 0~i 包括i的最长严格递增子序列的长度。
lens = len(nums)
dp = [1 for _ in range(lens)]

for i in range(1,lens):
    for j in range(i-1, -1, -1):
        if nums[i] > nums[j]:
            dp[i] = max(dp[i], dp[j] + 1)
    if res < dp[i]:
        res = dp[i]
return res

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 输入的字符串只含有小写英文字符。

思路

## 思路
本题和动态规划:718. 最长重复子数组 (opens new window)区别在于这里不要求是连续的了,但要有相对顺序,即:"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

继续动规五部曲分析如下:

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

有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么?

这样定义是为了后面代码实现方便,如果非要定义为长度为[0, i]的字符串text1也可以,我在 动态规划:718. 最长重复子数组 (opens new window)中的「拓展」里 详细讲解了区别所在,其实就是简化了dp数组第一行和第一列的初始化逻辑。

确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[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-1, j - 2]与text2[i-2, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

代码如下:

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]);
}
dp数组如何初始化
先看看dp[i][0]应该是多少呢?

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

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

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

代码:

确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:

1143.最长公共子序列

那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。

举例推导dp数组
以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图:

1143.最长公共子序列1

最后红框dp[text1.size()][text2.size()]为最终结果

以上分析完毕,python 代码如下:

code python 1

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str):
        len1, len2 = len(text1), len(text2)
        res = 0
        # 初始化数组
        dp = [[0 for _ in range(len2 + 1)] for _ in range(len1 + 1)]
        for i in range(len1):
            for j in range(len2):
                if text1[i] == text2[j]:
                    dp[i+1][j+1] = dp[i][j] + 1
                else:
                    dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])

                if res < dp[i+1][j+1]:
                    res = dp[i+1][j+1]
        return res

code python 2

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str):
        len1, len2 = len(text1), len(text2)
        # 初始化数组
        dp = [[0 for _ in range(len2 + 1)] for _ in range(len1 + 1)]
        for i in range(len1):
            for j in range(len2):
                if text1[i] == text2[j]:
                    dp[i+1][j+1] = dp[i][j] + 1
                else:
                    dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])

                if res < dp[i+1][j+1]:
                    res = dp[i+1][j+1]
        return dp[-1][-1]

1035.不相交的线

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。

思路

绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图:
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)
这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!
其实本题就是求最长公共子序列的长度,介于我们刚刚讲过动态规划:1143.最长公共子序列,所以本题我就不再做动规五部曲分析了。l

code python


class Solution:
    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        len1, len2 = len(nums1), len(nums2)
        dp = [[0 for _ in range(len2+1)] for _ in range(len1+1)]
        for i in range(len1):
            for j in range(len2):
                if nums1[i] == nums2[j]:
                    dp[i+1][j+1] = dp[i][j] + 1
                else:
                    dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
        return dp[-1][-1]

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

思路

连续
dp[j] : 0-j 包括j的最大连续和
dp[j]  = dp[j-1] + nums[j], nums[j],
lens = len(nums)
dp = [0 for _ in eange(lens)]
dp[0] = -2
res = 0
dp[1] = , -2 + 1,  1 = 1  res = 1
dp[2] = 1 - 3, - 3 = -2
dp[3] = -2 + 4, 4 = 1 res = 4
dp[4] = 4 - 1, - 1 = 3 res = 3
dp[5] = 3 + 2,  2 = 5 res = 5
dp[6] = 6, 1 = 6 res =6
dp[7] = 1, -5 = 1
dp[8] = 1 + 4, 4 = 5


code python

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        lens = len(nums)
        if lens ==1 :return nums[0]
        dp = [0 for _ in range(lens)]
        dp[0] = nums[0]
        res = nums[0]

        for i in range(1, lens):
            dp[i] = max(dp[i-1]+ nums[i], nums[i])
            if res < dp[i]:
                res = dp[i]
        return res

贪心 code python

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        res =float('-inf')
        pre_sum = 0
        for i in range(0, len(nums)):
            pre_sum += nums[i]
            if pre_sum > res:
                res = pre_sum
            if pre_sum <= 0:
                pre_sum = 0
        return res

from 代码随想录学习

这道题之前我们在讲解贪心专题的时候用贪心算法解决过一次,贪心算法:最大子序和 。
这次我们用动态规划的思路再来分析一次。

动规五部曲如下:
确定dp数组(dp table)以及下标的含义
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。

确定递推公式
dp[i]只有两个方向可以推出来:
dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
nums[i],即:从头开始计算当前连续子序列和
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

dp数组如何初始化
从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。

dp[0]应该是多少呢?

根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。

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

举例推导dp数组
以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下: 53.最大子序和(动态规划)

注意最后的结果可不是dp[nums.size() - 1]! ,而是dp[6]。

在回顾一下dp[i]的定义:包括下标i之前的最大连续子序列和为dp[i]。

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

所以在递推公式的时候,可以直接选出最大的dp[i]。

以上动规五部曲分析完毕,完整代码如下:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = [0] * len(nums)
        dp[0] = nums[0]
        result = dp[0]
        for i in range(1, len(nums)):
            dp[i] = max(dp[i-1] + nums[i], nums[i]) #状态转移公式
            result = max(result, dp[i]) #result 保存dp[i]的最大值
        return result

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值