[Leetcode]动态规划之基本技巧&子序列问题&背包问题——python版本

本篇文章根据labuladong的算法小抄汇总动态规划(子序列问题和背包问题)的常见算法,采用python3实现

一、动态规划基本技巧

  • 一般形式:求最值,如最长递增子序列,最小编辑距离等

  • 核心问题:穷举

  • 动态规划三要素:

    • 重叠子问题:备忘录DP数组优化穷举过程,避免不必要的计算

    • 最优子结构:可以从子问题的最优结果推出更大规模问题的最优结果

    • ⭐状态转移方程:明确base case → 明确[状态] → 明确[选择] → 定义dp数组/函数的定义

      如,img

      #初始化base case
      dp[0][0][...] = base
      #进行状态转移
      for 状态1 in 状态1的所有取值:
          for 状态2 in 状态2的所有取值:
              for ...
              	dp[状态1][状态2][...] = 求最值(选择1,选择2,...)
      
  • dp数组的遍历方向

    #正向遍历
    for i in range(m):
        for j in range(n):
            #计算dp[i][j]
    
    #反向遍历
    for i in range(m-1,-1,-1):
        for j in range(n-1,-1,-1):
            #计算dp[i][j]
    
    #斜向遍历
    for l in range(2,n+1):
        for i in range(0,n):
            j = l + i - 1
            #计算dp[i][j]
    
    • 遍历过程中,所需的状态必须是已经计算出来的
    • 遍历的终点必须是存储结果的那个位置
  • 状态压缩技巧:

    • 针对:二维dp问题
    • 特点:状态转移方程,如果计算状态 d p [ i ] [ j ] dp[i][j] dp[i][j]需要的都是 d p [ i ] [ j ] dp[i][j] dp[i][j]相邻的状态,就可以使用状态压缩技巧,将二维的dp数组转化成一维,将空间复杂度从O(N^2)降到O(N)

斐波那契数列

1、带备忘录的递归解法

def fib(N):
    memo = [0 for i in range(N+1)]
    return helper(memo,N)
def helper(memo,n):
    if (n == 0) or (n == 1):
        return n
    if memo[n] != 0:
        return memo[n]
    memo[n] = helper(memo,n-1) + help(memo,n-2)
    return memo[n]

2、dp数组的迭代解法

def fib(n):
    if (n == 0) or (n == 1):
        return n
    dp = [0 for i in range(n+1)]
    dp[0] = 0
    dp[1] = 1
    for i in range(2,n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

3、状态压缩的dp数组

def fib(n):
    if (n == 0) or (n == 1):
        return n
    prev = 0
    curr = 1
    for i in range(2,n+1):
        sum = prev + curr
        prev = curr
        curr = sum
    return curr

零钱兑换

1、带备忘录的递归解法

#O(kn)
def coinChange(coins,amount):
    def dp(coins,amount):
        if amount < 0:
            return -1
        if amount == 0:
            return 0
        if memo[amount] != 0:
            return memo[amount]
        res = float("inf")
        for coin in coins:
            subProblem = dp(coins,amount-coin)
            if subProbkem == -1:
                continue
            res = min(res,subProblem+1)
        memo[amount] = res if res != float("inf") else -1
        return memo[amount]
    
    memo = [0 for i in range(amount+1)]
    return dp(coins,amount)

2、dp数组的迭代解法

def coinChange(coins,amount):
    dp = [amount + 1 for i in range(amount+1)]
    dp[0] = 0
    for i in range(amount+1):
        for coin in coins:
            if i - coin < 0:
                continue
            dp[i] = min(dp[i],1+dp[i-coin])
    return dp[amount] if dp[amount] != (amount+1) else -1

下降路径最小和

def minFallingPathSum(matrix):
    def dp(matrix,i,j):
        if (i < 0) or (j < 0) or (i >= len(matrix)) or (j >= len(matrix[0])):
            return float("inf")
        if i == 0:
            return matrix[0][j]
        if memo[i][j] != float("inf"):
            return memo[i][j]
        memo[i][j] = matrix[i][j] + min(dp(matrix,i-1,j-1),dp(matrix,i-1,j),dp(matrix,i-1,j+1))
        return memo[i][j]
        
    n = len(matrix)
    res = float("inf")
    memo = [[float("inf") for i in range(n)] for j in range(n)]
    for j in range(n):
        res = min(res,dp(matrix,n-1,j))
    return res

二、子序列类型问题

编辑距离

def minDistance(word1,word2):
    def dp(i,j):
        if i == -1:
            return j + 1
        if j == -1:
            return i + 1
        if s1[i] == s2[j]:
            return dp(i-1,j-1)
        else:
            return min(dp(i,j-1)+1,dp(i-1,j)+1,dp(i-1,j-1)+1)
    return dp(len(word1)-1,len(word2)-1)

加备忘录:

def minDistance(word1,word2):
    def dp(i,j):
        if i < 0:
            return j + 1
        if j < 0:
            return i + 1
        if (i,j) in memo:
            return memo[(i,j)]
        if word1[i] == word2[j]:
            memo[(i,j)] = dp(i-1,j-1)
        else:
            memo[(i,j)] = min(dp(i,j-1)+1,dp(i-1,j)+1,dp(i-1,j-1)+1)
        return memo[(i,j)]
    memo = dict()
    return dp(len(word1)-1,len(word2)-1)

dp数组

#dp数组定义:dp[i][j]表示从word1[0...i]到word2[0..j]需要变动的最小次数
def minDistance(word1,word2):
    m = len(word1)
    n = len(word2)
    dp = [[0 for i in range(n+1)] for j in range(m+1)]
    for i in range(1,m+1):
        dp[i][0] = i
    for j in range(1,n+1):
        dp[0][j] = j
    for i in range(1,m+1):
        for j in range(1,n+1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(dp[i-1][j-1]+1,dp[i][j-1]+1,dp[i-1][j]+1)
    return dp[m][n]

最长递增子序列

  • 子串一定连续,子序列不一定连续
#O(N^2)
#dp数组定义:dp[i]表示以nums[i]结尾的最长递增子序列
def lengthOfLIS(nums):
    n = len(nums)
    dp = [1 for i in range(n)]
    for i in range(n):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i],dp[j]+1)
    return max(dp)

俄罗斯套娃信封问题

  • 先对宽度w进行升序排序,若w相同,则对高度h降序排序。之后把所有h作为一个数组,在这个h数组上计算LIS的长度

    #envelopes = [[w,h],[w,h],...]
    def maxEnvelopes(envelopes):
        def lengthOfLIS(nums):
            n = len(nums)
            dp = [1 for i in range(n)]
            for i in range(n):
                for j in range(i):
                    if nums[i] > nums[j]:
                        dp[i] = max(dp[i],dp[j]+1)
            return max(dp)
        n = len(envelopes)
        #排序
    	envelopes.sort(key = lambda a: (a[0],-a[1]))
        #height数组
        height = [0 for i in range(n)]
        for i in range(n):
            height[i] = envelopes[i][1]
        return lengthOfLIS(height)
    

最大子序和

#dp数组定义:dp[i]表示以nums[i]结尾的最大子数组和
#时间复杂度O(N),空间复杂度O(N)
def maxSubArray(nums):
    n =len(nums)
    if n == 0:
        return 0
    dp = [0 for i in range(n)]
    dp[0] = nums[0]
    for i in range(1,n):
        dp[i] = max(nums[i],nums[i]+dp[i-1])
    return max(dp)

状态压缩版本:

def maxSubArrat(nums):
    n = len(nums)
    if n == 0:
        return 0
    dp_0 = nums[0]
    dp_1 = 0
    res = dp_0
    for i in range(1,n):
        dp_1 = max(nums[i],nums[i]+dp_0)
        dp_0 = dp_1
        res = max(res,dp_1)
    return res

最长公共子序列(LCS)

  • 对于两个字符串求子序列的问题,都是用两个指针i和j分别在两个字符串上移动,大概率是动态规划思路

备忘录:

def longestCommonSubsequence(text1,text2):
    #计算s1[i...]和s2[j...]的最长公共子序列长度
    def dp(s1,i,s2,j):
        if (i == len(s1)) or (j == len(s2)):
            return 0
        if memo[i][j] != -1:
            return memo[i][j]
        if s1[i] == s2[j]:
            memo[i][j] = 1 + dp(s1,i+1,s2,j+1)
        else:
            memo[i][j] = max(
                #s1[i]不在lcs中
                dp(s1,i+1,s2,j),
                #s2[j]不在lcs中
                dp(s1,i,s2,j+1))
        return memo[i][j]
        
    m = len(text1)
    n = len(text2)
    memo = [[-1 for i in range(n)] for j in range(m)]
    return dp(text1,0,text2,0)

dp数组:

def longestCommonSubsequence(text1,text2):
    m = len(text1)
    n = len(text2)
    dp = [[0 for i in range(n+1)] for j in range(m+1)]
    for i in range(1,m+1):
        for j in range(1,n+1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = 1 + dp[i-1][j-1]
            else:
                dp[i][j] = max(dp[i][j-1],dp[i-1][j])
    return dp[m][n]

两个字符串的删除操作

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        def longestCommonSubsequence(s1,s2):
            m = len(s1)
            n = len(s2)
            dp = [[0 for i in range(n+1)] for j in range(m+1)]
            for i in range(1,m+1):
                for j in range(1,n+1):
                    if s1[i-1] == s2[j-1]:
                        dp[i][j] = 1 + dp[i-1][j-1]
                    else:
                        dp[i][j] = max(dp[i-1][j],dp[i][j-1])
            return dp[m][n]
        m = len(word1)
        n = len(word2)
        r = longestCommonSubsequence(word1,word2)
        return m - r + n - r

两个字符串的最小ASCII删除和

def minimumDeleteSum(s1,s2):
    #dp:将s1[i..]和s2[j..]删除成相同字符串
    def dp(s1,i,s2,j):
        res = 0
        m = len(s1)
        n = len(s2)
        if i == m:
            for x in range(j,n):
                res += ord(s2[x])
            return res
        if j == n:
            for x in range(i,m):
                res += ord(s1[x])
            return res
        if memo[i][j] != -1:
            return memo[i][j]
        if s1[i] == s2[j]:
            memo[i][j] = dp(s1,i+1,s2,j+1)
        else:
            memo[i][j] = min(
                ord(s1[i]) + dp(s1,i+1,s2,j)
                ord(s2[j]) + dp(s1,i,s2,j+1)
            )
        return memo[i][j]
            
    m = len(s1)
    n = len(s2)
    memo = [[-1 for i in range(n)] for j in range(m)]
    return dp(s1,0,s2,0)

子序列解题模板

思路一:一维dp数组

如最长递增子序列

n = len(array)
dp = [0 for i in range(n)]
for i in range(1,n):
    for j in range(i):
        dp[i] = 最值(dp[i],dp[j]+...)

思路二:二维dp数组

n = len(arr)
dp = [[0 for i in range(n)] for j in range(n)]
for i in range(n):
    for j in range(1,n):
        if arr[i] == arr[j]:
            dp[i][j] = dp[i][j] + ...
        else:
            dp[i][j] = 最值(...)
  • 涉及两个字符串/数组时(如最长公共子序列),dp数组含义:

    在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]

  • 只涉及一个字符串/数组时(如最长回文子序列),dp数组含义:

    在子数组array[i…j]中,我们要求的子序列的长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]

最长回文子序列

def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0 for i in range(n)] for j in range(n)]
    for i in range(n):
        dp[i][i] = 1
    for i in range(n-1,-1,-1):
        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+1][j],dp[i][j-1])
    return dp[0][n-1]

状态压缩:

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

三、背包类型问题

0-1背包问题

问题:给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

背包问题的状态:背包的容量&可选择的物品 → 二维dp数组

背包问题的选择:装进背包or不装进背包

步骤:

1、明确状态和选择

2、明确dp数组的定义,包括base case和最终想得到的答案

3、根据选择,思考状态转移的逻辑

#dp[i][w]:对于前i个物品,当前背包容量为w,可以装的最大价值
def knapsack(W,N,wt,val):
    dp = [[0 for i in range(W+1)] for j in range(N+1)]
    for i in range(1,N+1):
        for w in range(1,W+1):
            if w - wt[i-1] < 0:
                dp[i][w] = dp[i-1][w]
            else:
                dp[i][w] = max(dp[i-1][w],dp[i-1][w-wt[i-1]]+val[i-1])
    return dp[N][W]

子集背包问题:分割等和子集

#dp[i][j]=x:对于前i个物品,当前背包容量为j,若x为True表示可以恰好将背包装满,否则不能
def canPartition(nums):
    sumNum = sum(nums)
    if sum % 2 != 0:
        return False
    n = len(num)
    sumNum = n // 2
    dp = [[False for i in range(sumNum+1)] for j in range(n+1)]
    for i in range(n+1):
        dp[i][0] = True
    for i in range(1,n+1):
        for j in range(1,sumNum+1):
            if j < nums[i-1]:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]]
    return dp[n][sumNum]

状态压缩:

def canPartition(nums):
    sumNum = sum(nums)
   	if sumNum % 2 != 0:
        return False
    n = len(nums)
    sumNum = sumNum // 2
    dp = [False for i in range(sumNum+1)]
    dp[0] = True
    for i in range(n):
        for j in range(sumNum,-1,-1):
            if j - nums[i] >= 0:
                dp[j] = dp[j] or dp[j-nums[i]]
    return dp[sumNum]

完全背包问题:零钱兑换

完全背包问题:每个物品的数量是无限的

#dp[i][j]:如果只使用前i个物品(可以重复使用),当背包容量为j时,有dp[i][j]种方法可以装满背包
def change(amount,coins):
    n = len(coins)
    dp = [[0 for i in range(amount+1)]for j in range(n+1)]
    for i in range(n+1):
        dp[i][0] = 1
    for i in range(1,n+1):
        for j in range(1,amount+1):
            if j < coins[i-1]:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
    return dp[n][amount]

目标和

1、回溯

def findTargetSumWays(nums,target):
    def backtrack(nums,i,rest):
        if len(nums) == i:
            if rest == 0:
                self.res += 1
            return
        rest -= nums[i]
        backtrack(nums,i+1,rest)
        rest += nums[i]
        
        rest += nums[i]
        backtrack(nums,i+1,rest)
        rest -= nums[i]
                
    self.res = 0
    backtrack(nums,0,target)
    return self.res

2、备忘录消除重叠子问题

def findTargetSumWays(nums,target):
    def dp(nums,i,rest):
        if len(nums) == i:
            if rest == 0:
                return 1
            return 0
        key = str(i) + "," + str(rest)
        if key in memo:
            return memo[key]
        result = dp(nums,i+1,rest-nums[i])+dp(nums,i+1,rest+nums[i])
        memo[key] = result
        return result
    
    if len(nums) == 0:
        return 0
    memo = dict()
    return dp(nums,0,target)

3、动态规划

转化为子集划分问题,转化为背包问题

如果把nums分成两个子集A和B,分别代表分配+的数和分配-的数,那么:

sum(A) - sum(B) = target
sum(A) = target + sum(B)
2 * sum(A) = target + sum(nums)

于是原问题转化为:nums中存在几个子集A,使其元素和为(target+sum(nums))/2

#dp[i][j]=x:只在前i个物品中选择,若当前背包的容量为j,则最多有x种方法可以恰好装满背包
def findTargetSumWays(nums,target):
    def subsets(nums,target):
        n = len(nums)
        dp = [[0 for i in range(target+1)] for j in range(n+1)]
        for i in range(n+1):
            dp[i][0] = 1
        for i in range(1,n+1):
            for j in range(target+1): #注意从0开始,否则target为1时,range(1,1)为空
                if j < nums[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
        return dp[n][target]
                
    sumNum = sum(nums)
    if (sumNum < abs(target)) or ((sumNum+target) % 2 != 0): #注意abs,否则会造成dp数组为空
        return 0
    return subsets(nums,(sumNum+target)//2)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值