Datawhale编程实践LeetCode分类练习——Task02:动态规划(Python)

动态规划常常适用于有重叠子问题最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

打家劫舍系列

198. 打家劫舍

198. 打家劫舍(简单)
在这里插入图片描述

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

        for i in range(2,n):
            dp[i] = max(dp[i-1],dp[i-2]+nums[i])

        return dp[-1]

只与前两个数有关,因此可以进行如下优化

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n==0:
            return 0       
        if n==1:
            return nums[0]
        
        prev2 = nums[0] #i-2,dp[0]
        prev1 = max(nums[0],nums[1]) #i-1,dp[1]
        
        for i in range(2,n):
            temp = max(prev1,prev2+nums[i])
            prev2 = prev1
            prev1 = temp

        return prev1

213. 打家劫舍II

213. 打家劫舍II(中等)
在这里插入图片描述
所有房屋围成一圈,即第一间和最后一间房屋不能同时偷窃,因此分为两种情况

  1. 第一间到倒数第二间
  2. 第二间到最后一间
  3. 取max
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        if n==1:
            return nums[0]
        if n==2:
            return max(nums[0],nums[1])
        
        return max(self.dp(nums[:-1]),self.dp(nums[1:]))

    def dp(self,nums):
        prev2 = 0
        prev1 = 0

        for i in range(0,len(nums)):
            temp = max(prev2+nums[i],prev1)
            prev2 = prev1
            prev1 = temp
        return prev1

337. 打家劫舍 III

337. 打家劫舍 III(中等)
在这里插入图片描述
思路:
为每个节点设置两个数,分别表示偷/不偷时的最高金额;

  • 偷,则左右子节点都只能不偷
  • 不偷,则取左右子节点的max再求和
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def rob(self, root: TreeNode) -> int:
        if not root:
            return 0
        
        res = self.helper(root)
        return max(res)

    
    def helper(self,root):
        if not root:
            return (0,0)
        if root.left==None and root.right==None:
            return (root.val,0)
        left = self.helper(root.left)
        right = self.helper(root.right)
        res = [0,0]
        res[0] = left[1]+right[1]+root.val
        res[1] = max(left[0],left[1])+max(right[0],right[1])
        return res

LeetCode一维dp问题

300. 最长上升子序列,LIS

300. 最长上升子序列(中等)

在这里插入图片描述

首先,需要对「子序列」和「子串」这两个概念进行区分;

  • 子序列(subsequence)
    子序列并不要求连续,例如:序列 [4, 6, 5] 是 [1, 2, 4, 3, 7, 6, 5] 的一个子序列。
  • 子串(substring、subarray)
    子串一定是连续的,例如:「力扣」第 3 题:“无重复字符的最长子串”,「力扣」第 53 题:“最大子序和”。

方法一: 动态规划
定义dp数组,dp[i]表示以nums[i]为结尾的最长上升子序列的长度

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n<=1:
            return n
        
        dp = [1 for _ in range(n)]
        res = 1
        for i in range(1,n):
            for j in range(i-1,-1,-1):
                if nums[i]>nums[j]:
                    dp[i] = max(dp[i],dp[j]+1)
                    res = max(res,dp[i])
        
        return res

复杂度分析:
时间复杂度:O(n2)
空间复杂度:O(n),额外的dp数组

方法二: 贪心+二分
思路:

  • 依旧着眼于上升子序列结尾的元素
  • 已经得到的上升子序列结尾元素越小,构成更长的上升子序列的可能性更大
  • 在上升子序列长度固定的情况下,记录结尾最小的元素的数组tail

tail数组的长度就是最长上升子序列的长度

过程:
遍历nums;

  1. 如果nums[i]>tail[-1],nums[i]加入tail数组;
  2. 否则,找到tail中第一个大于等于nums[i]的数(二分),使它变小。
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n<=1:
            return n
        
        tail = [nums[0]]
        for i in range(1,n):
            if nums[i]>tail[-1]:
                tail.append(nums[i])
                continue
            
            l = 0
            r = len(tail)-1
            flag = True
            while l<=r:
                mid = (l+r)>>1
                if tail[mid]==nums[i]:
                    flag = False
                    break
                elif tail[mid]>nums[i]:
                    r = mid-1
                else:
                    l = mid+1
            if flag:
                tail[l] = nums[i]
        
        return len(tail)

参考题解区大佬

674. 最长连续递增序列

674. 最长连续递增序列(简单)
在这里插入图片描述

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

压缩空间,优化如下:

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n<2:
            return n
        
        prev = 1
        res = 1
        for i in range(1,n):
            if nums[i]>nums[i-1]:
                temp = prev+1
                
            else:
                temp = 1
            prev = temp
            res = max(res,temp)
        
        return res

LeetCode二维dp问题

1143. 最长公共子序列,LCS

1143. 最长公共子序列在这里插入图片描述
在这里插入图片描述
思路:
定义二维数组,dp[i][j]表示text1[0:i]和text2[0:j]的最长公共子序列长度。
转移方程

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        if not text1 or not text2:
            return 0
        
        n1 = len(text1)
        n2 = len(text2)

        dp = [[0 for _ in range(n2+1)] for _ in range(n1+1)]

        for i in range(1,n1+1):
            for j in range(1,n2+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 dp[-1][-1]

只与前一行和本行中前面的元素有关,dp压缩为2*(n2+1)

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        if not text1 or not text2:
            return 0
        
        n1 = len(text1)
        n2 = len(text2)

        dp = [[0 for _ in range(n2+1)] for _ in range(2)]
        i = 1
        for i in range(1,n1+1):
            for j in range(1,n2+1):
                if text1[i-1]==text2[j-1]:
                    dp[i%2][j] = dp[(i-1)%2][j-1]+1
                else:
                    dp[i%2][j] = max(dp[(i-1)%2][j],dp[i%2][j-1])
        
        return dp[i%2][-1]

5. 最长回文子串

5. 最长回文子串(中等)
在这里插入图片描述
一、确定动态规划状态
用两个指针来记录子串的位置——二维dp数组记录
题目要求返回最长的回文子串——dp数组记录True/False
dp[i][j]表示子串s从i到j是否为回文子串

二、状态转移方程

  • 子串首尾字符不相等时,肯定不是回文
  • 子串首尾字符相等时
    • 如果s[i+1][j-1]是回文,则s[i][j]是回文;
    • 否则不是;
    • 如果s[i+1][j-1]是空串或长度为1,则子串是回文(j-1-(i+1)+1<2,即j-i<3)
if s[i]==s[j]:
	if j-i<3:
		dp[i][j]=True
	else:
		dp[i][j]=dp[i+1][j-1]

注意填表顺序!!!

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if len(s)<=1:
            return s
        
        n = len(s)
        dp = [[False for _ in range(n)]for _ in range(n)]
        max_len = 1#记录最长的长度
        start = 0 #记录最长回文子串的初始位置
        for j in range(1,n):
            for i in range(j):
                if s[i]==s[j]:
                    if j-i<3:
                        dp[i][j]=True
                    else:
                        dp[i][j] = dp[i+1][j-1]
                if dp[i][j]:
                    cur_len = j-i+1
                    if cur_len>max_len:
                        max_len = cur_len
                        start = i
        return s[start:start+max_len]

516. 最长回文子序列

516. 最长回文子序列(中等)
在这里插入图片描述

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        if n<2:
            return n
        
        dp = [[0 for _ in range(n)] for _ in range(n)]#dp[i][j]
        
        for i in range(n):
            dp[i][i] = 1

        for j in range(1,n):
            for i in range(j-1,-1,-1):
                if s[i]!=s[j]:
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1])
                else:
                    dp[i][j] = dp[i+1][j-1]+2
        
        return dp[0][-1]

72. 编辑距离

72. 编辑距离(困难)
在这里插入图片描述
一、确定动态规划状态
设计两个字符串,考虑二维dp数组来保存转移状态;
定义dp[i][j]表示word1[0:i]转化为word2[0:j]所执行的最少操作次数。

二、状态转移方程
word1[i] == word2[j]时,dp[i][j]=dp[i-1][j-1]
word1[i] != word2[j]时,dp[i][j]=1+min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])

  1. 插入:dp[i][j-1]+1
  2. 删除:dp[i-1][j]+1
  3. 替换:dp[i-1][j-1]+1
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        if not word1 and not word2:
            return 0
        
        l1 = len(word1)
        l2 = len(word2)

        dp = [[0 for _ in range(l2+1)] for _ in range(l1+1)]

        for i in range(l1+1):
            dp[i][0] = i
        for j in range(1,l2+1):
            dp[0][j] = j

        for i in range(1,l1+1):
            for j in range(1,l2+1):
                if word1[i-1]==word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j]=1+min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])
        #print(dp)
        return dp[-1][-1]

参考资料:
https://github.com/datawhalechina/team-learning-program/blob/master/LeetCodeClassification/2.%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92.md

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值