Task02 动态规划

最长回文串

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

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

解题思路:回文串本身就具有状态转移的性质:删除回文串的头尾两个字符还是回文串,因此就可以使用动态规划的方法解决。
我们使用dp[i][j]保存状态(以下用P(i,j)表示),表示字符串s的第i到j个字符组成的子串是否为回文串:
在这里插入图片描述

这里的其他情况包含可能性:
1.子串s_i…s_j本身不是回文串;
2.i>j,此时子串不合法。
因此,动态规划的状态转移方程为:
在这里插入图片描述

即只有 s[i+1:j−1] 是回文串,并且 s 的第 i 和 j 个字母相同时,s[i:j] 才会是回文串。
上述的情况建立在子串长度大于2的前提,当子串长度为1,必然是个回文串;当子串长度为2,需要判断两个字符是否相同。因此我们就可以写出动态规划的边界条件:
在这里插入图片描述

最终我们返回所有状态为True中的最大长度子串即为最长回文串。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        #建立一个二维数组保存状态
        dp = [[False] * n for _ in range(n)]
        ans = ""

        #枚举子串长度为L+1
        for L in range(n):
            # 枚举子串的起始位置 i,这样可以通过 j=i+L 得到子串的结束位置
            for i in range(n):
                #j为子串的结束位置
                j = i+L
                #结束位置超过最大长度停止
                if j >= len(s):
                    break
                #长度为1的子串为回文串
                if L == 0:
                    dp[i][j] = True
                #长度为2的子串判断两个字符是否相同
                elif L == 1:
                    dp[i][j] = (s[i] == s[j])
                #状态转移方程
                else:
                    dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
                #返回最大长度的回文串
                if dp[i][j] and L + 1 > len(ans):
                    ans = s[i:j+1]
       
        return ans

编辑距离

给你两个单词 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’)

解题思路:我们从末尾看起,如果最后一个字符相同,那么只需对之前的字符进行操作,否则操作次数+1,对之前的字符操作时,选择操作次数最小的一种,然后重复这个过程。
首先理解编辑的含义:
1.如果word1[i] == word2[j],那么比较word1[0…i-1]和word2[0…j-1] ;
2.否则,有三个选项:
a.执行插入操作,在word1末尾插入word2[j]后,末尾相同,只需比较之前的字符,即word1[0…i]和word2[0…j-1];
b.执行删除操作,删除word[i]后,比较word1[0…i-1]和word2[0…j];
c.执行替换操作,替换后比较word1[0…i-1]和word2[0…j-1]
最后在上述三个结果中次数最少的+1
由于涉及了子问题,可以使用自顶向下的递归或者自底向上的动态规划。
在递归的求解方式中,有太多的重复计算,可以新建一个二维数组保存状态进行优化。
动态规划求解则需先写出状态转移方程,因为是两个字符串,有两个对应的状态,我们建立一个二维数组dp[i][j]保存状态,根据上述的解释,我们可以得到:
1.若word1[i] == word2[j],那么dp = dp[i-1][j-1]
2.否则,dp[i][j] = 1 + min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n = len(word1)
        m = len(word2)

        #其中一个字符串为空串时,相当于在空串中插入另一个字符串
        if n * m == 0:
            return n + m
        #创建二维数组保存状态
        dp = [[0] * (m+1) for _ in range(n+1)]

        #边界状态初始化,即当前长度的字符串插入到空串所需的次数
        for i in range(n+1):
            dp[i][0] = i
        for j in range(m+1):
            dp[0][j] = j

        #根据状态转移方程计算所有dp值
        for i in range(1, n+1):
            for j in range(1, m+1):
                #如果最后一个字符不同,需要多一步操作,否则直接计算之前的字符串
                if word1[i-1] != word2[j-1]:
                    dp[i-1][j-1] += 1
                dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1] - 1)

        return dp[n][m]

时间复杂度:O(mn),即为遍历两个字符串数组长度。
空间复杂度:O(mn),需要额外开创一个数组记录状态。

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

解题思路:如果只有一间屋子,那么最高金额就是这间房的金额;如果只有两间房屋,由于相邻的房屋无法同时偷窃,因此最高金额就是这两间房的最大值;当房间数量大于2时,我们就要分情况考虑了。
当K>2时,对于第K间房,我们有偷窃或不偷窃两个选择:
1.偷窃第K间房,无法偷窃第K-1间房,最高金额为当前第K间房的金额以及前K-2间房的最高总金额;
2.不偷窃第K间房,最高金额为前K-1间房的最高总金额。
由于只有一个状态进行转移,使用一位数组dp[i]表示前i间房能偷窃到的最高总金额,状态转移方程为:
在这里插入图片描述

有了方程我们还需要考虑边界条件:
在这里插入图片描述

最后我们需要的答案就是dp[size-1],其中size为数组长度。

class Solution:
    def rob(self, nums: List[int]) -> int:
        #一间房都没有,偷了个寂寞
        if not nums:
            return 0
        #房子数量
        size = len(nums)
        #只有一间房,只能偷他家了
        if size == 1:
            return nums[0]
        #创建数组保存状态
        dp = [0] * size
        #设置边界条件
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        #执行动态转移
        for i in range(2, size):
            dp[i] = max(nums[i] + dp[i-2], dp[i-1])
        
        return dp[size-1]

上述方法使用了额外数组存储结果,其实每间房屋的最高总金额之和该房子前两间房的最高总金额相关,因此可以使用滚动数组,每个时刻只存储前两间房的最高总金额:
对于[2,7,9,3,1]
[2,7],first=2,second=max(2,7)=7
[2,7,9],first=second=7,second = max(2+9,second)=11
[2,6,9,3],first=second=11,second=max(7+3,second)=11
[2,7,9,3,1],first=second=11, second=max(11+1, second)=12
最终返回second=12

class Solution:
    def rob(self, nums: List[int]) -> int:
        #一间房都没有,偷了个寂寞
        if not nums:
            return 0
        #房子数量
        size = len(nums)
        #只有一间房,只能偷他家了
        if size == 1:
            return nums[0]
        
        first, second = nums[0], max(nums[0], nums[1])
        for i in range(2, size):
            first, second = second, max(first + nums[i], second)
        
        return second

时间复杂度:O(n),遍历一次数组
空间复杂度:O(1),使用滚动数组的方法则不再需要额外开创一个数组。

打家劫舍||

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

解题思路:此题中的房间是环装排列的,其实就是在上一题的基础上掐头或者去尾,因此我们可以吧数据分为两组,其中一组不包含头,求出最大金额p1,另一组不包含尾,求出最大金额p2,分别使用上一节的方法求解后,取两者的最大值max(p1,p2)。

class Solution:
    def rob(self, nums: List[int]) -> int:
        #一间房都没有,偷了个寂寞
        if not nums:
            return 0
        #房子数量
        size = len(nums)
        #只有一间房,只能偷他家了
        if size == 1:
            return nums[0]
        #否则将数组分为两组
        nums1 = nums[:size-1]
        nums2 = nums[1:]
        size1 = len(nums1)
        size2 = len(nums2)
        if size1 == 1 and size2 == 1:
            return max(nums1[0], nums2[0])
        
        first1, second1 = nums1[0], max(nums1[0], nums1[1])
        for i in range(2, size1):
            first1, second1 = second1, max(first1 + nums1[i], second1)

        first2, second2 = nums2[0], max(nums2[0], nums2[1])
        for i in range(2, size2):
            first2, second2 = second2, max(first2 + nums2[i], second2)
        
        return max(second1, second2)

代码又臭又长,可以把中间将数组分为两组的部分略去,直接写在循环条件中:

class Solution:
    def rob(self, nums: List[int]) -> int:
        #一间房都没有,偷了个寂寞
        if not nums:
            return 0
        #房子数量
        size = len(nums)
        #只有一间房,只能偷他家了
        if size == 1:
            return nums[0]
        #否则将数组分为两组
        first1, second1 = 0, 0
        for num in nums[:-1]:
            first1, second1 = second1, max(first1 + num, second1)

        first2, second2 = 0, 0
        for num in nums[1:]:
            first2, second2 = second2, max(first2 + num, second2)
        
        return max(second1, second2)

还是有代码复用的情况,可以写个函数:

class Solution:
    def rob(self, nums: List[int]) -> int:
        def maxrob(mynums):
            first, second = 0, 0
            for num in mynums:
                first, second = second, max(first + num, second)
            return second
        return max(maxrob(nums[:-1]), maxrob(nums[1:])) if len(nums) != 1 else nums[0]

最长回文子序列

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

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

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

提示:
1 <= s.length <= 1000
s 只包含小写英文字母

解题思路:与子串不同,子序列不要求连续,只要求顺序一致,如abcd中abd是子序列,虽然不连续但顺序一致,而acb则不是,因为与原本字符串的顺序不同。
结合之前做过的最长回文子串,可以想到一个字符串如果头尾相同,那么就看去掉头尾之后的中间部分是否为回文,否则,就看去掉头的字符串以及去掉尾的字符串,分别查看是否为回文。这就有了一个动态转移的过程,题目只要求我们输出回文子序列的最大长度,我们可以构造动态转移方程:
dp[i][j]=dp[i+1][j-1] + 2 if(str[i]==str[j])
dp[i][j]=max(dp[i+1][j],dp[i][j-1]) if (str[i]!=str[j])
这里的边界条件为dp[i][i] = 1,每个字符串本身都是一个回文子序列。
需要注意的是i需要从大到小逆序遍历,因为计算dp[i][j]时需要计算dp[i+1][*],而j则从小到大遍历,计算的是第i个字符到第j个字符,只要保证j比i大即可:
在这里插入图片描述

最后返回的结果即为dp[0][n-1],n为数组长度

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        #计算dp[i][j]时需要计算dp[i+1][*]或dp[*][j-1],因此i应该从大到小,即递减;j应该从小到大,即递增
        for i in range(n-1, -1, -1):
            dp[i][i] = 1
            #计算的是字符i到字符j,需要保证j比i大
            for j in range(i+1, n):
                #如果头尾两个字符相同,则计算中间的字符
                if s[i] == s[j]:
                    dp[i][j] = dp[i+1][j-1] + 2
                #否则分别计算去掉头和尾的两组字符串,再取其中最大值
                else:
                    dp[i][j] = max(dp[i][j-1], dp[i+1][j])

        
        return dp[0][n-1]

其中也看出动态规划四个要素:
1.状态dp,在该题中为最长回文子序列的长度;
2.转移方程,即归纳,从已知结果中推出未知部分,比如已经知道子问题的dp[i+1][j-1](该题中为s[i+1:j-1]中最长回文子序列的长度)的结果,如何推出dp[i][j],如该题中我们发现这取决于前后两个字符s[i]和s[j]是否相等;
3.边界条件,最基本的情况是什么;
4.返回结果,根据dp数组的定义和题目要求返回。
再者,还有思路模板,分别是一维和二维的dp数组,视需要保存的状态数而定:
模板1:

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

模板2:

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] = 最值(...)
    }
}

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

最长连续递增序列

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

示例 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。

注意:数组长度不会超过10000。

解题思路:直接遍历,逐一比较,然后统计最长的连续递增序列就好了。做法很简单,主要还是思考如何优化。
滑动窗口法:
每个连续递增的子序列是不相交的,一个数在第一个递增子序列出现后,不可能在另一个递增子序列出现,因此就有一个边界,也就是nums[i-1]≥nums[i]的时候,递增子序列就停止了,开始计算下一个递增子序列,我们把此时的i保存在一个变量count中。候选答案为i-count+1,每次遍历更新候选答案的最大值。

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:     
        ans = count = 0
        for i in range(len(nums)):
            if i and nums[i-1] >= nums[i]:
                count = i
            ans = max(ans, i - count + 1)
            
        return ans

动态规划:
定义dp[i]为连续递增序列长度。
边界情况为:
1.当nums[i-1]≥nums[i]时,dp[i]=1
2.dp[0] = 1
状态转移方程:dp[i] = dp[i-1] + 1
返回:max(dp)

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:     
        n = len(nums)
        if n == 0:
            return 0
        dp = [0] * n
        dp [0] = 1
        for i in range(n):
            dp[i] = dp[i-1] + 1 if nums[i] > nums[i-1] else 1
        
        return max(dp)

总结

动态规划难在思维,需要通过归纳总结出一个合适的状态转移方程。一般解题按如下步骤:
第一步:确定动态规划状态

第二步:写出状态转移方程

第三步:考虑初始化条件

第四步:考虑输出状态

第五步:考虑对时间,空间复杂度的优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值