作为一类极其重要的动态规划问题,将其单独分类。

0-1 背包

01-背包:板子题

给定背包问题 - 图1件物品,其体积重量分别为背包问题 - 图2背包问题 - 图3。求在总体积背包问题 - 图4的限制下,能够装下的最大重量背包问题 - 图5

  1. def bag_01(V, volumes, weights):
  2. """
  3. 给定n件物品的体积与价值,考虑总体积<=V时,能够获得的最大价值
  4. """
  5. n = len(volumes)
  6. # 考虑前i件物品,容量不超过j时的最大价值
  7. dp = [[0 for _ in range(V + 1)] for _ in range(n + 1)]
  8. for i in range(1, n + 1):
  9. for j in range(V + 1):
  10. volume, weight = volumes[i-1], weights[i-1]
  11. pick = dp[i-1][j-volume] + weight if j>=volume else 0 # 能选 or 不能选
  12. not_pick = dp[i-1][j]
  13. dp[i][j] = max(pick, not_pick) # 选 or 不选
  14. return dp[-1][-1]
  • 时间复杂度:背包问题 - 图6
  • 空间复杂度:背包问题 - 图7
  1. def bag_01_opt(V, volumes, weights):
  2. dp = [0 for _ in range(V + 1)]
  3. for volume, weight in zip(volumes, weights):
  4. for j in range(V + 1):
  5. pick = dp[j - volume] + weight if j >= volume else 0
  6. not_pick = dp[j]
  7. dp[j] = max(pick, not_pick)
  8. return dp[-1]
  • 时间复杂度:背包问题 - 图8
  • 空间复杂度:背包问题 - 图9

416. Partition Equal Subset Sum

背包问题 - 图10:使用背包问题 - 图11的所有元素,能否凑到金额背包问题 - 图12
由于每个数字只能使用一次,故转化为0-1背包问题。

  • 选择num背包问题 - 图13
  • 不选择num背包问题 - 图14不变

    1. class Solution:
    2. def canPartition(self, nums: List[int]) -> bool:
    3. if (total := sum(nums)) & 1:
    4. return False
    5. target = total // 2
    6. n = len(nums)
    7. # dp[i][j]: 使用前i个数字,能否恰好凑到和j
    8. dp = [[True] + [False] * (target) for _ in range(n + 1)]
    9. for i in range(1, n + 1):
    10. num = nums[i-1]
    11. for j in range(1, target + 1):
    12. # 选第i个数字 => 前i-1个数字能否凑到和j-num
    13. pick = dp[i-1][j-num] if j >= num else False
    14. not_pick = dp[i-1][j] # 不选第i个数字
    15. dp[i][j] = pick or not_pick
    16. return dp[-1][-1]
  • 时间复杂度:背包问题 - 图15

  • 空间复杂度:背包问题 - 图16

    1. class Solution:
    2. def canPartition(self, nums: List[int]) -> bool:
    3. total = sum(nums)
    4. if total & 1:
    5. return False
    6. target = total // 2 # 建模:等分数组 => 能否凑到数组的一半
    7. dp = [True] + [False] * target # 滚动数组
    8. for num in nums:
    9. for j in range(target, num-1, -1): # 倒序遍历,注意范围
    10. dp[j] |= dp[j - num]
    11. return dp[-1]
  • 时间复杂度:背包问题 - 图17

  • 空间复杂度:背包问题 - 图18

474. Ones and Zeroes

背包问题 - 图19:考虑前背包问题 - 图20字符串背包容量背包问题 - 图21背包问题 - 图22背包问题 - 图23背包问题 - 图24时,能容纳的字符串数量最大值。

  1. class Solution:
  2. def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
  3. dp = [[[0] * (n+1) for _ in range(m+1)] for __ in range(len(strs)+1)]
  4. for i in range(1, len(strs) + 1):
  5. zeros = strs[i-1].count('0')
  6. ones = len(strs[i-1]) - zeros
  7. for j in range(m + 1):
  8. for k in range(n + 1):
  9. if j < zeros or k < ones:
  10. dp[i][j][k] = dp[i-1][j][k]
  11. else:
  12. dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-zeros][k-ones] + 1)
  13. return dp[-1][-1][-1]
  1. class Solution:
  2. def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
  3. dp = [[0] * (n+1) for _ in range(m+1)]
  4. for s in strs:
  5. zeros = s.count('0')
  6. ones = len(s) - zeros
  7. for i in range(m, zeros-1, -1):
  8. for j in range(n, ones-1, -1):
  9. dp[i][j] = max(dp[i][j], dp[i-zeros][j-ones] + 1)
  10. return dp[-1][-1]
  1. from functools import lru_cache
  2. class Solution:
  3. def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
  4. @lru_cache(None)
  5. def dfs(i, m, n):
  6. if i == len(strs):
  7. return 0
  8. zeros = strs[i].count('0')
  9. ones = len(strs[i]) - zeros
  10. not_pick = dfs(i + 1, m, n)
  11. pick = dfs(i + 1, m - zeros, n - ones) + 1 if m >= zeros and n >= ones else 0
  12. return max(not_pick, pick)
  13. return dfs(0, m, n)

494. Target Sum

背包问题 - 图25 背包问题 - 图26 背包问题 - 图27
背包容量背包问题 - 图28的限制下,求装满背包的总方案数。注意容量背包问题 - 图29的背包初始化方案数为1
特判:

  1. 背包问题 - 图30不可整除背包问题 - 图31
  2. 背包问题 - 图32的绝对值超过了背包问题 - 图33,即全部置为正数,也不能满足要求。

    1. class Solution:
    2. def findTargetSumWays(self, nums: List[int], target: int) -> int:
    3. n, total = len(nums), sum(nums)
    4. if abs(target) > total or (total + target) & 1:
    5. return 0
    6. pos_sum = (total + target) // 2
    7. # dp[i]: 凑成和为i的正数的方案数
    8. dp = [1] + [0] * pos_sum
    9. for num in nums:
    10. for i in range(pos_sum, num-1, -1): # target -> num 逆向遍历
    11. dp[i] += dp[i - num]
    12. return dp[-1]
  • 时间复杂度:背包问题 - 图34
  • 空间复杂度:背包问题 - 图35
  1. class Solution:
  2. def findTargetSumWays(self, nums: List[int], target: int) -> int:
  3. @lru_cache(None)
  4. def dfs(idx, total):
  5. if idx == len(nums):
  6. return 1 if total == target else 0
  7. pos = dfs(idx + 1, total + nums[idx])
  8. neg = dfs(idx + 1, total - nums[idx])
  9. return pos + neg
  10. return dfs(0, 0)
  • 时间复杂度:背包问题 - 图36,遍历所有背包问题 - 图37种表达式,每个表达式计算结果需要背包问题 - 图38的时间
  • 空间复杂度:背包问题 - 图39,取决于递归调用栈的深度

完全背包

完全背包:板子题

背包问题 - 图40件物品和一个容量为背包问题 - 图41的背包,每件物品有无限件;第背包问题 - 图42件物品的体积质量分别为背包问题 - 图43背包问题 - 图44
求解如何将物品装入背包,能够使得容纳相应物品的情况下,装下的质量最大。

  1. def bag_cmp(N, C, volumes, weights):
  2. # 考虑前i件物品,装入容量为j的背包 => 最大价值W:dp[i][j]
  3. dp = [[0 for _ in range(C + 1)] for _ in range(N + 1)]
  4. for i in range(1, N + 1):
  5. volume, weight = volumes[i-1], weights[i-1]
  6. for j in range(C + 1):
  7. pick = dp[i][j-volume] + weight if j >= volume else 0 # 同一row进行转移
  8. not_pick = dp[i-1][j]
  9. dp[i][j] = max(pick, not_pick)
  10. return dp[-1][-1]
  1. def bag_cmp_opt(N, C, volumes, weights):
  2. dp = [0 for _ in range(C + 1)]
  3. for volume, weight in zip(volumes, weights):
  4. for j in range(volume, C + 1): # 起点为volume
  5. pick = dp[j - volume] + weight
  6. not_pick = dp[j]
  7. dp[j] = max(pick, not_pick)
  8. return dp[-1]

322. Coin Change

背包问题 - 图45:(考虑前背包问题 - 图46枚硬币,可复用)填满容量背包问题 - 图47的背包需要的最少硬币数量。

  1. class Solution:
  2. def coinChange(self, coins: List[int], amount: int) -> int:
  3. dp = [0] + [10001] * amount
  4. for coin in coins:
  5. for j in range(coin, amount+1):
  6. dp[j] = min(dp[j], 1 + dp[j-coin])
  7. return dp[-1] if dp[-1] != 10001 else -1
  • 时间复杂度:背包问题 - 图48
  • 空间复杂度:背包问题 - 图49

518. Coin Change 2

背包问题 - 图50:(考虑前背包问题 - 图51枚硬币,可复用)填满容量背包问题 - 图52的背包的最多方案数。

  1. class Solution:
  2. def change(self, amount: int, coins: List[int]) -> int:
  3. dp = [1] + [0] * amount
  4. for coin in coins:
  5. for j in range(coin, amount + 1):
  6. dp[j] += dp[j - coin]
  7. return dp[-1]
  • 时间复杂度:背包问题 - 图53
  • 空间复杂度:背包问题 - 图54

279. Perfect Squares

  1. class Solution:
  2. def numSquares(self, n: int) -> int:
  3. # dp[j]:凑成数字j需要的最少正整数数量
  4. dp = [0] * (n + 1)
  5. for i in range(n + 1):
  6. dp[i] = i
  7. for i in range(2, 100 + 1): # i:当前选择的正整数[1, 100]
  8. if i**2 > n:
  9. break
  10. for j in range(i**2, n + 1):
  11. dp[j] = min(dp[j], dp[j-i**2] + 1)
  12. return dp[-1]
  • 时间复杂度:背包问题 - 图55
  • 空间复杂度:背包问题 - 图56