代码随想录算法训练营Day53 | 1143. 最长公共子序列 | 1035. 不相交的线 | 53. 最大子序和 (动态规划)

文章目录

  • 1143. 最长公共子序列
  • 1035. 不相交的线
  • 53. 最大子序和 (动态规划)

1143. 最长公共子序列

题目链接 | 解题思路

Subsequence: a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

本题和之前的最长重复子数组很相似,区别仅仅只是子序列并不像子数组一样要求连续。但实际上,这个区别直接影响了 dp 数组的定义。

  1. dp 数组的下标含义:dp[i][j]text1[:i+1]text2[:j+1] 的最长公共子序列长度

    • 定义相比于之前发生了变化,由于不再要求连续的重复,也就不再需要以当前元素 text1[i], text2[j] 结尾这个条件,可以直接进行正常的状态转移
  2. dp 递推公式:

    • 如果 text1[i] == text2[j],那么显然 dp[i][j] = dp[i-1][j-1] + 1
    • 否则,尝试移去某一个元素来利用之前的状态进行递推:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  3. dp 数组的初始化:由于在递推公式中可能用到了 dp[i-1][j], dp[i][j-1],显然我们要初始化第一列 dp[i][0] 和第一行 dp[0][j]

    • i=0 的时候,根据定义,dp[0][j]text1[0]text2[:j+1] 的最长公共子序列的长度,所以如果 text1[0] == text2[k],这一行之后所有的 dp[0][j]=1 ( j ≥ k ) (j\geq k) (jk)
    • j=0 的时候,同理
  4. dp 数组的遍历顺序:从左到右、从上到下,正常遍历即可

  5. 举例推导:text1 = "abcde", text2 = "ace"

    a c e
    a 1 1 1
    b 1 1 1
    c 1 2 2
    d 1 2 2
    e 1 2 3
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        # dp[i][j] represents the max common-subsequence length between text1[:i+1] and text2[:j+1]
        dp = [[0] * len(text2) for _ in range(len(text1))]

        first_row_flag = first_col_flag = False
        for i in range(len(text1)):
            if text1[i] == text2[0]:
                first_row_flag = True
            if first_row_flag:
                dp[i][0] = 1
        for j in range(len(text2)):
            if text1[0] == text2[j]:
                first_col_flag = True
            if first_col_flag:
                dp[0][j] = 1

        result = 1 if first_row_flag or first_col_flag else 0

        for i in range(1, len(text1)):
            for j in range(1, len(text2)):
                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])
                if dp[i][j] > result:
                    result = dp[i][j]

        return result

注意,由于这里的 dp 数组定义发生了变化,最终的结果一定是 dp[-1][-1]。所以不需要在遍历过程中不断记录最大值了。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        # dp[i][j] represents the max common-subsequence length between text1[:i+1] and text2[:j+1]
        dp = [[0] * len(text2) for _ in range(len(text1))]

        first_row_flag = first_col_flag = False
        for i in range(len(text1)):
            if text1[i] == text2[0]:
                first_row_flag = True
            if first_row_flag:
                dp[i][0] = 1
        for j in range(len(text2)):
            if text1[0] == text2[j]:
                first_col_flag = True
            if first_col_flag:
                dp[0][j] = 1

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

1035. 不相交的线

题目链接 | 解题思路

第一次看到这道题目的描述真的被吓傻了,看上去甚至像是一道数学题。由此可见,对题目的抽象能力,有时候和解题能力一样重要。

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

经过上述抽象化,可以发现,本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!和上一题完全一样,只要改一下变量名即可。然而想要完成这样的抽象化真的需要敏锐的嗅觉和对 dp 的深刻理解啊。

不过值得一提的是,我在解题时虽然没有直接想到这个抽象化方法,但是也能够通过 nums1[i]nums2[j] 的比较来得到正确的递推公式(dp 数组的定义还是比较好想的)。显然,我们会很希望 nums1[i] == nums2[j],这样就可以直接使用之前的 dp[i-1][j-1]。否则,我们只能希望去掉其中一个末尾元素可以找到“尾部连线”的状态。在得到正确的递推公式之后,也就能够逐渐理解题意了。

class Solution:
    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        # dp[i][j] represents the max number of crosslines between nums1[:i+1] and nums2[:j+1] 
        dp = [[0] * len(nums2) for _ in range(len(nums1))]

        first_row_flag = first_col_flag = False
        for i in range(len(nums1)):
            if nums1[i] == nums2[0]:
                first_row_flag = True
            if first_row_flag:
                dp[i][0] = 1
        for j in range(len(nums2)):
            if nums1[0] == nums2[j]:
                first_col_flag = True
            if first_col_flag:
                dp[0][j] = 1
        result = 1 if first_row_flag or first_col_flag else 0

        for i in range(1, len(nums1)):
            for j in range(1, len(nums2)):
                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])
                if dp[i][j] > result:
                    result = dp[i][j]
        
        return result

53. 最大子序和 (动态规划)

题目链接 | 解题思路

之前用贪心算法解过,但实际上贪心算法比 dp 还要复杂。虽然是一道一维的子序列,但还是很好体现了 dp 利用当前元素递推子问题的特性。

  1. dp 数组的下标定义:dp[i] 记录的是 nums[:i+1] 中以 nums[i] 为结尾的最大连续子数组的和
  2. dp 递推公式:直接判定当前值即可,dp[i] = max(dp[i-1] + nums[i], nums[i])
  3. dp 数组的初始化:由于 dp[i] 可以由 dp[i-1] 直接推导,只需要关注 dp[0] 的初始化即可。根据定义,显然 dp[0] = nums[0]
  4. dp 的遍历顺序:从前到后即可
  5. 举例推导:[-2,1,-3,4,-1,2,1,-5,4]
-2 1 -3 4 -1 2 1 -5 4
-2 1 -2 4 3 5 6 1 5
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = [float('-inf')] * len(nums)
        dp[0] = nums[0]
        for i in range(1, len(nums)):
            dp[i] = max([dp[i-1] + nums[i], nums[i]])
        return max(dp)

在我做贪心算法的时候我就用这个 dp 解出来了,难道我真的是天才?

你可能感兴趣的:(代码随想录算法训练营一刷,算法,动态规划)