动态规划-背包问题

三种背包问题

背包问题主要分为三种:

  • 0-1 背包问题:
    • 定义:给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
    • 变种的子集背包问题定义:给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
  • 完全背包问题:
    • 定义:0-1 背包问题中,每个物品最多可以装一次。完全背包中,所有物品的数量是无限的。
    • 因为物品的数量没有限制,因此使用基于贪心策略来做。 循环判断「剩余空间下可容纳的最高性价比物品」,并加入背包。

0-1背包问题

题目 416. 分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def canPartition(self, nums: List[int]) -> bool:
nums_sum = sum(nums)
half_sum = nums_sum // 2
if half_sum * 2 != nums_sum:
return False

m = len(nums)
# dp 定义:对于前 i 个物品(从1开始),空间 j 的情况下,是否可以放满
dp = [[False for _ in range(half_sum + 1)] for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = True

for i in range(1, m + 1):
for j in range(1, half_sum + 1):
# 如果空间小于当前物品空间
if j < nums[i - 1]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i - 1]])
return dp[m][half_sum]

题目 494. 目标和

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-‘ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-‘ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:

def findTargetSumWays(self, nums: List[int], target: int) -> int:
# sum(a) = (target + sum(nums)) / 2
# 从 nums 中选择一组数,使其相加为 (target + sum(nums)) / 2,问,有多少种方法
if sum(nums) < abs(target) or (sum(nums) + target) % 2 == 1:
return 0

return self.subset(nums, (sum(nums) + target) // 2)

def subset(self, nums, target):
m = len(nums)
# 前 i 个数,填满 j 个空间的方法
dp = [[0 for _ in range(target + 1)] for _ in range(m + 1)]
# 前 0 个数,占满前 0 个空间的方式为1个。
# 需注意,dp[i][0] 不可以都初始化为 1,比如 [1,1,1,1] 变成 0 就有多种方法,因此下边的 j 需要从 0 开始。
dp[0][0] = 1

for i in range(1, m + 1):
for j in range(0, target + 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[m][target]

题目 1049. 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

1
2
3
4
5
6
7
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
"""
题目可以抽象为:石头重量之间进行 +、- 符号的组合, 使用最后的结果最小。
记:石头的总重量为 sum、+ 的石头总重量为 pos、 - 的石头总重量为 neg:
-> pos = sum - neg
-> pos - neg = sum - 2 * neg
-> sum - 2 * neg 取最小值时,满足题目要求。
-> 为满足题目要求, neg 需要在不超过 sum/2 的前提下,尽可能的大。

-> 最终题目转化为,在 stones 在 sum/2 最多可以占用的空间
"""

m = len(stones)
total = sum(stones)
n = total // 2

# 定义 dp[i][j] 为前 i 个石头是否可以凑出重量 j
dp = [[False for _ in range(n + 1)] for _ in range(m + 1)]

for i in range(m + 1):
# 只要不选择任何石头,就可以凑出 0,所有的 dp[i][0] 均为 true
dp[i][0] = True

for i in range(1, m + 1):
for j in range(1, n + 1):
if j < stones[i - 1]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - stones[i - 1]]

# 找到 dp[m] 行中,最后一个为 1 的位置,此时即为 neg 的值,带入 sum - 2 * neg
ans = None
for j in range(n, -1, -1):
if dp[m][j]:
ans = total - 2 * j
break
return ans

完全背包问题

题目 518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

示例 1:

1
2
3
4
5
6
7
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
m = len(coins)

# 定义 dp,对前 i 个物品,空间 j 的情况下,有多少种凑满的方式
dp = [[0 for _ in range(amount + 1)] for _ in range(m + 1)]

for i in range(m + 1):
# 只要不选择任何钱币,就可以凑出 0
dp[i][0] = 1

for i in range(1, m + 1):
for j in range(1, amount + 1):
if j - coins[i - 1] < 0:
# 额度不足,当前硬币不能使用。
dp[i][j] = dp[i - 1][j]
else:
# 凑满方式分为:不使用第 i 个物品的凑满 + 使用第 i 个物品的凑满
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
return dp[m][amount]
作者

mmmwhy

发布于

2023-01-20

更新于

2023-02-02

许可协议

评论