【leetcode】动态规划:各类最长子序列、子串、数组等问题

目录

写在前面

常见题型

例题分析

674. 最长连续递增序列

300. 最长上升子序列

673. 最长递增子序列的个数

 5. 最长回文子串

516. 最长回文子序列 

1143. 最长公共子序列

 718. 最长重复子数组


写在前面

动态规划一般有两种思路:

1、第一种思路模板是一个一维的 dp 数组:

int n = array.length;
int[] dp = new int[n];

for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}

例如:「最长递增子序列」,在这个思路中 dp 数组的定义是:以i结尾(包括i在内)的数组最长的递增子序列的长度为 dp[i]

2、第二种思路模板是一个二维的 dp 数组:

int n = arr.length;
int[][] dp = new dp[n][n];

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

运用相对更多一些,尤其是涉及两个字符串/数组的子序列。

常见题型

这里把leetcode常见的采用动态规划的各类最长序列、字符串、数组等问题罗列出来,做一个归纳。

  • 输入为单个字符串或者数组
名称动态方程含义
最长连续递增(上升)序列(数组)一维dp[i],表示截止到i这个数字(必须包括i)的最长连续上升序列的长度
最长上升子序列(数组)一维dp[i],表示截止到i这个数字(必须包括i)的最长上升子序列的长度
最长上升子序列个数(数组)两个一维dp[i],conunt[i],分别表示截止到i这个数字(必须包括i)的最长上升子序列的长度以及个数
最长回文子串(字符串)二维dp[j][i],表示j-i区间构成的子串是否为回文子串
最长回文子序列(字符串)二维dp[j][i],表示j-i区间构成的子串的回文子序列的长度

可以看到,也就是「最长连续递增序列」以及「最长递增子序列」采用了一维的dp,其余都是二维的dp。这两个问题也是最为简单。

  • 输入为两个字符串和数组
名称动态方程含义
最长重复子数组二维dp[j][i],表示截止s[:j]与s[:i]的重复子数组的长度
最长公共子序列二维dp[j][i],表示截止s[:j]到s[:i]的最长公共子序列的长度

例题分析

674. 最长连续递增序列

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

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

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

 要点:

(1)因为每一个数字都可与自身构成连续递增,故动态数组dp初始化1。

(2)只要关注当前位置i与其前一个的位置i-1的值的大小:

  • nums[i]>nums[i-1]:i至少可以与i-1形成一个连续递增序列,因为它们俩挨着,并且是递增的,长度上是dp[i-1]+1;
  • nums[i]<=nums[i-1],这时候不能形成连续递增序列,后一个数要比前一个数小,呈下降的趋势,与自身形成连续递增
     
    def findLengthOfLCIS(self, nums):
        if not nums:
            return 0
        length = len(nums)
        dp = [1 for i in range(length)] #创建一维的动态数组
        for i in range(1,length):
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1]+1
        return max(dp)

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。如:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

要点:

(1)这里定义dp[i]为截止到i这个数字,并包含i在内的最长上升子序列的长度。

(2)因为每一个数字都可与自身构成上升,故动态数组dp初始化1 。

(3) 如果nums[i] > nums[j],那么 nums[i] 一定能够融入 dp[j] 从而形成更大的序列,这个序列的长度是 dp[j] + 1,即为状态转移方程。

    def lengthOfLIS(self, nums):
        length = len(nums)
        dp = [1 for i in range(length)]
        for i in range(length):
            for j in range(i-1,-1,-1):  #也可以是for j in range(0,i)
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i],dp[j]+1)
        return max(dp)

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

例1:输入: [1,3,5,4,7]
          输出: 2
          解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

例2:输入: [2,2,2,2,2]
          输出: 5
          解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

本题在上一题的基础上继续求取最长子序列的个数。因此动态方程有两个,dp[i]和count[j],分别表示截止到i这个数字(必须包括i)的最长上升子序列的长度以及个数。

要点:

(1)每一个数字都可与自身构成连续递增,故动态数组dp初始化1 ,同时个数count也等于1。

(2)count状态转移方程:

  • 当dp[j] + 1>dp[i],说明第一次找到以nums[i]为结尾的最长递增子序列,长度为dp[j] + 1,进而可以推出counter[i] = counter[j], 即以nums[i]结尾的最长递增子序列的组合数=以nums[j]结尾的最长递增子序列的组合数。这个可以这么理解:当nums[i] > nums[j],即就是在count[j]每一种组合结尾补上nums[i],对于组合数本身是没有增加的,唯独只是递增子序列的长度+1了;
  • 当dp[j] + 1=dp[i],说明这个长度已经找到过一次了,counter[i] += counter[j],即现在的组合方式+counter[j]的组合方式。
    def findNumberOfLIS(self, nums):
        length = len(nums)
        dp = [1 for i in range(length)]
        count = [1 for i in range(length)]
        for i in range(length):
            for j in range(i-1,-1,-1):#也可以是for j in range(0,i)
                if nums[i] > nums[j]:
                    if dp[j] + 1 > dp[i]:
                        dp[i] = dp[j]+1
                        count[i] = count[j]
                    elif dp[j] + 1 == dp[i]:
                        count[i] = count[i] + count[j]
        maxlength = max(dp)
        res = 0
        for i in range(length):
            if dp[i] == maxlength:
                res = res + count[i]
        return res

 5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

例 1:输入: "babad"
           输出: "bab"
           注意: "aba" 也是一个有效答案。
例 2:输入: "cbbd"
           输出: "bb"

 要点:

(1)解决这类问题的核心思想就是两个字“延伸”,具体来说:

  • 如果一个字符串是回文串,那么在它左右分别加上一个相同的字符,那么它一定还是一个回文串
  • 如果在一个不是回文字符串的字符串两端添加任何字符,或者在回文串左右分别加不同的字符,得到的一定不是回文串

(2)用 dp[i][j] 表示 s 中从 i 到 j(包括 i 和 j)是否可以形成回文,状态转移方程只是将上面的描述转化为代码即可:

if s[i] === s[j] and dp[i + 1][j - 1]:
  dp[i][j] = true
def longestPalindrome(self, s):
        length = len(s)
        dp = [[False for i in range(length)] for j in range(length)]
        logestLen = 0
        logestSubStr = '' 
        for i in range(length):
            for j in range(i,-1,-1):  #写成for j in range(0,i+1) 也可
                if i-j <= 1:
                    if s[i] == s[j]:
                        dp[j][i] = True
                        if i - j + 1 > logestLen:
                            logestSubStr = s[j:i+1] 
                            logestLen = i - j + 1   
                else:
                    if s[i] == s[j] and dp[j+1][i-1] == True:
                        dp[j][i] = True
                        if i - j + 1 > logestLen:
                            logestSubStr = s[j:i+1] 
                            logestLen = i - j + 1 
        return logestSubStr   

516. 最长回文子序列 

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

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

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

 与上面最长回文子串不同,最长回文子序列不要求连续,比如对于输入bbbab,最长回文子序列为bbbb,长度为4。

要点:

(1)dp[j][i]表示s[j⋯i]中,最长的回文序列长度是多少。

  • 当s[i] = s[j],那么就说明在原先的基础上又增加了回文子序列的长度,dp[j][i] = dp[j+1][i-1] + 2
  •  当s[i] !=s[j],那么那么说明s[i]、s[j]至少有一个不在回文子序列中,这时的dp[j][i]只需取两者之间的最大值即可。即dp[j][i] = max(dp[j+1][i],dp[j][i-1])。

(2)第二个循环的遍历顺序必须为倒序。由于我们设定i>j,因此当j>i时,dp = 0。在计算dp[j][i]时就必须要确保左边dp[j][i-1]、下边dp[j+1][i]以及左下边dp[j+1][i-1]的数值是存在的

以计算s[0:2]之间的最长回文子序列dp[0][2]为例,第二个循环如果采用正序的话,由于s[0]=c,不等于s[2]=b,dp[0][2] = max(dp[1][2],dp[0][1]),而此时dp[1][2]还没有被计算出来,故会出错的。而采用倒序的话,则首先会计算s[1:2]之间的最长回文子序列dp[1][2],之后在计算dp[0][2],即求出了s[0:2]之间的最长回文子序列。

 ps:在最长回文子串中,第二个循环之所以也可以用正序,是因为当s[j]=s[i]时,如果dp[j+1][i-1]为True,那么dp[j][i]也为True,即就是说dp[j][i]只需要由左下角的dp[j+1][i-1]来确定,而正序条件下,左下角的dp[j+1][i-1]都可以被预先算出来

    def longestPalindromeSubseq(self, s: str) -> int:
        length = len(s)
        dp =[[0] * length for _ in range(length)]   #dp[j][i]表示 s的 j个字符到第i个字符组成的子串中,最长的回文序列长度是多少。
        for i in range(length):
            dp[i][i] = 1
            for j in range(i-1,-1,-1):
                if s[j] == s[i]: #两者相等
                    dp[j][i] = dp[j+1][i-1] + 2
                else:  #两者不等于
                    dp[j][i] = max(dp[j+1][i],dp[j][i-1])               
        return dp[0][length-1]

1143. 最长公共子序列

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

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列,因为相对顺序发生了改变。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

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

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

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

要点
(1)动态数组dp[i][j]表示的是截止到text1[:i]与text1[:j]的最长公共子序列。所谓子序列是不需要连续的。
(2)动态数组dp[i][j]更新如下:

  • 如果text[i] == text[j]表明多了一共公共的字符,因此dp[i][j] = dp[i-1][j-1] + 1
  • 如果text[i] != text[j],那么就在text1[:i-1]与text1[:j] 或者 text1[:i]与text1[:j-1]的公共子序列中取最大值,即dp[i][j] = max(dp[i-1][j],dp[i][j-1])

(3)在生成动态dp的过程中,我们多加了一行一列的原因在于,dp[i-1]很可能定位到矩阵的最后一行。例如求取'bm'和'mb'两个字符串的最长公共序列,正常答案为1,可以是'b'或者'm'。用i表示text1截止的字符位置,j表示textj截止的字符位置。如果没有多加一列多加一行,经过第一次外面j的大循环,我们得到的dp如下,即dp[1][0]为1,因为text1 = ‘bm’,text2 = 'm',最长公共子序列长度为1。开始第二次j的大循环时,此时text2 = ‘mb’,text1 = ‘b’,dp[0][1] = dp[-1][0] + 1,而dp[-1][0]就是dp[1][0],因此dp[0][1]就等于dp[1][0] + 1 = 2。但是实际上,两者的最长公共子序列为1。

    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        if not text1 or not text2:
            return 0
        n1,n2 = len(text1),len(text2)
        dp = [[0 for j in range(n2+1)] for i in range(n1+1)]
        for j in range(1,n2+1):
            for i in range(1,n1+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])
        return max(max(dp))

 718. 最长重复子数组

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

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

 本题与上一题十分类似,不同之处在于所求的公共的长度最长的子数组是连续的,而前一题的子序列可以是不连续的。

要点:

(1)动态数组dp[i][j]表示的是截止到A[:i]与B[:j]的最长公共子数组,初始化为0,动态转移方程如下:

  • 若A[i] = B[j],说明多增加了一个连续,dp[i][j] = dp[i-1][j-1] + 1。
  • 若A[i] != B[j],说明此时不连续了,长度为0,即为初始化的0

(2)最后return 不要写max(max(dp))。由于dp是一个二维数组,储存的是截止到A[:i]与B[:j]的最长公共子数组,因此想当然就会直接return dp的最大值。但是内层的max(dp)返回的是dp子列表首元素最大的那个列表,之后外层的max再找出这一行的最大值。以A=[1,2,3,2,1],B=[3,2,1,4,7]为例,求得的dp如下,正确答案为3,但是内部max(dp)后会首先返回第4行,因为第4行的第2个元素为1,最大。之后在max一次就会返回1,导致错误。

    def findLength(self, A, B):
        if not A or not B:
            return 0
        n1,n2 = len(A),len(B)
        dp = [[0 for j in range(n2+1)] for i in range(n1+1)]
        ans = 0
        for j in range(1,n2+1):
            for i in range(1,n1+1):
                print(A[i-1],B[j-1])
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                    ans = max(ans,dp[i][j])
        return ans  #不要写max(max(dp))

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值