动态规划-子序列问题解题笔记

这篇博客探讨了动态规划在解决子序列问题中的应用,包括一维和二维数组的解题模板。讨论了最长递增子序列、最长公共子序列和最长回文子序列等问题,以及如何找到状态转移方程。同时,介绍了如何优化时间复杂度,如信封问题和编辑距离问题的解决方案。
摘要由CSDN通过智能技术生成

在这里插入图片描述
子序列的好多问题归结起来就是类似的转移关系。

找状态转移方程的方法是,思考每个状态有哪些「选择」,只要我们能用正确的逻辑做出正确的选择,算法就能够正确运行。
动态规划之子序列问题解题模板–两种思路
参考https://mp.weixin.qq.com/s/zNai1pzXHeB2tQE6AdOXTA

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] + ...)
    }
}

在子数组array[0…i]中,以array[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 = 1; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。

2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:

在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。

一维–53. 最大子序和

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

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

dp[i]的含义是如果加上更大,则更大。

class Solution(object):
    def maxSubArray(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        dp = [0] * n
        for i in range(n):
            if i == 0:
                dp[i] = nums[i]
                continue
            dp[i] = max(dp[i-1] + nums[i], nums[i])

        return max(dp)

信封问题

在这里插入图片描述

**信封问题的关键是排序,排序后和最长递增子序列其实是一样的。**先对长进行排序,再对宽进行排序,其实后续的操作就是针对宽的排序来进行。其实下面解法的复杂度是n*n,dp[i]代表的是第i个的最长递增,所以每一次都要遍历一下之前的,确保是最大的。 不遍历的话,可能存在以下情况: 1、 2、4.5、5、 4、 6、9、 7; 5对应的是4, 而4只对应了3。

a = [[1,3], [2, 9], [2,2], [1,5]]
a = sorted(a, key=lambda x:(x[0], x[1]))
print(a)
# dp[i] 代表nums[1:j]的最长序列
def envelope(nums):
    nums = sorted(nums, key=lambda x:(x[0], x[1]))
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(0, i):
            if i == 0:
                continue
            if nums[i][1] > nums[j][1]:
                if dp[i] <= dp[j]:
                    dp[i] = dp[j] + 1
    return dp[-1]
print(envelope(a))

涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。

最长递增子序列

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

    # dp[i]代表的是第i个数的最长子序列
    # 转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # if nums[i] > nums[i-1]: dp[i] = dp[i-1] + 1
        # dp[i]代表的是第i个数的最长子序列
        # 转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)
        dp = [0] * len(nums)
        for i in range(len(nums)):
            if i == 0:
                dp[i] = 1
            else:
                dp[i] = 1
                n = i - 1
                while n >= 0:
                    if nums[i] > nums[n]:
                        if dp[i] <= dp[n]:
                            dp[i] = dp[n] + 1
                    n -= 1
        return max(dp)

上述的时间复杂度是n*n。
但是面试肯定是要nlogn的:解决如下: 看图更容易理解:其实下图还是等级于蜘蛛纸牌的方法。如果5后面的3可以在5前面找到比5更小的,当然就不会改5了。而是改前面的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

优化:参考蜘蛛纸牌问题:按序从大到小进行堆放,如果大于所有堆顶,则新建一个堆,每次叠放优先选择最左边的堆顶。因此可以得到堆顶的牌其实都是有序的,从左到右递增,每个堆也是有序的。
在这里插入图片描述
nlogn—多少个堆就有多少个。此时out只记录堆顶的数即可,因为只需要堆顶和总的堆数。
二分问题其实有一点要注意

# 蜘蛛牌+二分
class Solution1:
    def lengthOfLIS(self, nums):
        if not nums:
            return 0
        n = len(nums)
        out = [nums[0]]
        for i in range(1, n):
            done = 0
            start = 0
            end = len(out)
            while start < end:
                mid = (start + end) // 2
                if nums[i] > out[mid]:
                    start = mid + 1
                elif nums[i] <= out[mid]:
                    done = 1
                    end = mid
            if done == 1:
                out[end] = nums[i]
            if done == 0:
                out.append(nums[i])
        return len(out)

1143. 最长公共子序列

讲解:https://mp.weixin.qq.com/s/SUJ35XDpTn5OKU7hud-tPw
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。使用了一个二维数组来记录。

首先明确好,跟上述的二维表示一样,dp[i][j]表示的是str1[1:i]和str2[1:j]的相同部分,可以由max(dp[i-1][j], dp[i][j-1],dp[i-1][j-1]来获得)—通过表格可以画出:
在这里插入图片描述
在这里插入图片描述
先用暴力的递归解决,再思考动态规划:

"""
# 超时,加上装饰器就可以通过。
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        import functools
        @functools.lru_cache(None)  
        def dp(i, j):
            if i < 0 or j < 0:
                return 0
            if text1[i] == text2[j]:
                return dp(i-1, j-1)+1
            else:
                return max(dp(i-1, j), dp(i, j-1))
        return dp(len(text1)-1, len(text2)-1)


class Solution: # 字典替代上述的装饰器。
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        record = {}
        def dp(i, j):
            if (i,j) in record:
                return record[(i,j)]
            if i < 0 or j < 0:
                record[(i,j)] = 0
                return 0
            if text1[i] == text2[j]:
                record[(i,j)] = dp(i-1, j-1)+1
                return record[(i,j)]
            else:
                record[(i,j)] = max(dp(i-1, j), dp(i, j-1))
                return record[(i,j)]
        return dp(len(text1)-1, len(text2)-1)
"""
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        dp = []
        if not text2 or not text1:
            return 0
        for i in range(len(text1)):
            dp.append([0]*len(text2))
        for i in range(len(text1)):
            for j in range(len(text2)):
                if text1[i] == text2[j]:
                    if i == 0 or j == 0:
                        dp[i][j] = 1
                    else:
                        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]

在这里插入图片描述

class Solution:
    def LCS(self , s1 , s2 ):
        # write code here "" += 无法过,直接赋可以过......
        if not s1:
            return "-1"
        if not s2:
            return "-1"
        dp = []
        m = len(s1)
        n = len(s2)
        res = []
        for i in range(m):
            dp.append([""]*n)
        
        for i in range(m):
            if s1[i] == s2[0]:
                 dp[i][0] = s1[i]
            else:
                if i == 0:
                    pass
                else:
                    dp[i][0] = dp[i-1][0]
                    
        for j in range(n):
            if s2[j] == s1[0]:
                dp[0][j] = s2[j]
            else:
                if j == 0:
                    pass
                else:
                    dp[0][j] = dp[0][j-1]
        
        for i in range(1, m):
            for j in range(1, n):
                len1 = len(dp[i-1][j])
                len2 = len(dp[i][j-1])
                if s1[i] == s2[j]:
                    len3 = len(dp[i-1][j-1]) + 1
                    len4 = max([len1, len2, len3])
                    if len4 == len1:
                        dp[i][j] = dp[i-1][j]
                    elif len4 == len2:
                        dp[i][j] = dp[i][j-1]
                    else:
                        dp[i][j] = dp[i-1][j-1] + s1[i]
                else:
                    if len1 > len2:
                        dp[i][j] = dp[i-1][j]
                    else:
                        dp[i][j] = dp[i][j-1]
        if not dp[-1][-1]:
            return "-1"
        return dp[-1][-1]

583. 两个字符串的删除操作

-----画表
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:
输入: “sea”, “eat”
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

提示:
给定单词的长度不超过500。
给定单词中的字符只含有小写字母。

重难点在于转移公式:

两种解法,第一种利用上面的最长公共子序列来进行,通过计算最长的之后,len(word1) + len(word2) - 2 * len(sub);

if  word1[i] == word2[j]:
	dp[i][j] = dp[i-1][j-1] + 1
else:
	dp[i][j] = max(dp[i-1][j], dp[i][j-1])

第二种是直接计算需要删除的次数,感觉理解还可以,需要注意的是存在空字符串的情况,所以定义dp数组大小的时候,注意加上一个维度,来表示""的情况,这样才可以把basecase写好。

if word1[i] == word2[j]:
	dp[i][j] = dp[i-1][j-1]
else:
	dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1

在这里插入图片描述
一 最长子序列

class Solution(object):
    def minDistance(self, word1, word2):
        """
        :type word1: str
        :type word2: str
        :rtype: int
        其实就是求最长,然后 len(word1) + len(word2) - 2 * len(sub)
        """

        if not word1 and not word2:
            return 0
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)

        dp = []
        for i in range(len(word1)):
            dp.append([0]*len(word2))
        
        for i in range(len(word1)):
            for j in range(len(word2)):
                if word1[i] == word2[j]:
                    if i == 0 or j == 0:
                        dp[i][j] = 1
                    else:
                        dp[i][j] = dp[i-1][j-1] + 1
                else:
                    if i-1>=0 and j-1>=0:
                        dp[i][j] = max(dp[i-1][j], dp[i][j-1])  
                    elif i-1>=0:                       
                        dp[i][j] = dp[i-1][j]
                    elif j-1>=0:
                        dp[i][j] = dp[i][j-1]
        return len(word1) + len(word2) - dp[-1][-1] * 2

二 直接删除

# 动态规划也可以直接作用在删除的次数上
class Solution(object):
    ###### 注意考虑为空的情况,所以dp加上一个维度!!!!!!!!!!关键!!!!!!
    def minDistance(self, word1, word2):
        if not word1 and not word2:
            return 0
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)

        dp = []
        for i in range(len(word1)+1):
            dp.append([0]*(1+len(word2)))

        for i in range(len(word1)+1):
            for j in range(len(word2)+1):
                if i == 0:
                    dp[0][j] = j
                    continue
                if j == 0:
                    dp[i][j] = i
                    continue

                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    if i-1>=0 and j-1>=0:
                        dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1

        return dp[-1][-1]

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

注意已经分成子问题了,每一个小的串都会考虑到,所以不需要担心 删除str1某个元素,如果str2存在该元素,那么操作数不就更多,其实不然~~因为这对元素的判断其实也考虑进来了—
这道题容易乱。但是想明白,其实就是str1的替换,删除,str2删除。每增加一个元素,判断str1[i]和str2[j]是否相同,相同的话,dp[i-1][j-1]相比dp[i-1][j]+1和dp[i][j-1]+1如果也是最少的操作,那么dp[i][j] = dp[i-1][j-1],如果不是最少,那么取另两个最少的+1; 如果不相同,可以是直接删除掉,反正就是从dp[i-1][j]和dp[i][j-1]转移过来,他们本身已经是相同的str了,选择操作数少的即可。

这道题虽然看似不简单,但是也就是跟上面一样的状态的转移,要看清楚,想明白dp[i][j],以及怎么转移,另外三个被转移的状态分别代表什么含义
虽然有删除有添加,但是其实
上述方法中,最后其实就是可以归结为:word1替换,word1删除,word2删除。
首先明确状态的含义:
dp[i][j]代表的是word1[:i+1]和word2[:j+1]之间的最小编辑距离。所以,dp[i][j]其实就是从dp[i-1][j],dp[i-1][j-1],dp[i][j-1]转移过来的。
相对于dp[i][j],dp[i-1][j]代表的是经过处理之后,word1[0:i]以及word2[:j+1]是完全一样的,那么此时dp[i][j]相比于dp[i-1][j]就是多了一个i,我们可以选择直接删除,或是在word2上加上word1[i],就是一个操作可以使得两者继续相等。同理,dp[i][j-1]也是一样的道理。
注意,dp[i-1][j-1]不一样的地方,如果word1[i] == word2[j],那么如果相比于前面两个,dp[i-1][j-1]是最小的,则dp[i][j] = dp[i-1][j-1];; 如果 word1[i] != word2[j], dp[i][j] = dp[i-1][j-1] + 1。

class Solution:
    """
    dp[i][j]代表word1[:i+1] 与word2[:j+1]之间的最小编辑距离
    """
    def minDistance(self, word1, word2):
        
        if not word1 and not word2:
            return 0
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)
        
        m = len(word1)
        n = len(word2)
        dp = []
        for i in range(m+1):
            dp.append([0]*(n+1))

        for i in range(m+1):
            for j in range(n+1):
                if i == 0:
                    dp[i][j] = j
                elif j == 0:
                    dp[i][j] = i
                else:
                    if word1[i-1] != word2[j-1]:
                        dp[i][j] = min(dp[i-1][j]+1, dp[i-1][j-1]+1, dp[i][j-1]+1)
                    else:
                        dp[i][j] = min(dp[i-1][j]+1, dp[i-1][j-1], dp[i][j-1]+1)
        return dp[-1][-1]



另外的解法:Labuladong
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

最小编辑代价

在这里插入图片描述

做这个题的关键是想好状态转移,现在的状态是基于之前的状态的,之前的状态就是已完成的状态.

class Solution:
    def minEditCost(self , str1 , str2 , ic , dc , rc ):
        if not str1:
            return len(str2) * ic
        elif not str2:
            return len(str1) * dc
        else:
            dp = []
            m = len(str1)
            n = len(str2)
            for i in range(m):
                dp.append([0]*n)
                
            for i in range(n):
                if i == 0:
                    if str1[0] == str2[0]:
                        dp[0][i] = 0
                    else:
                        dp[0][i] = min(rc, ic+dc)
                else:
                    dp[0][i] = dp[0][i-1]+ic
                        
            for i in range(m):
                if i == 0:
                    if str1[0] == str2[0]:
                        dp[0][0] = 0
                    else:
                        dp[0][0] = min(rc, ic+dc)
                else:
                    dp[i][0] = dp[i-1][0]+dc
                    
            for i in range(1, m):
                for j in range(1, n):
                    if str1[i] == str2[j]:
                        dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]+dc, \
                                       dp[i][j-1]+ic)
                    else:
                        dp[i][j] = min(dp[i-1][j-1]+rc, dp[i-1][j]+dc,\
                                       dp[i][j-1] + ic)
        return dp[-1][-1]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值