赞
踩
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i]
,价值为 val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?
例如
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
算法返回 6,选择前两件物品装进背包,总重量 3 小于 W,可以获得最大价值 6。
物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。
第一步要明确两点,「状态」和「选择」。
状态有两个,就是「背包的容量」和「可选择的物品」。
选择就是「装进背包」或者「不装进背包」嘛。
第二步要明确 dp 数组的定义。
刚才找到的「状态」,有两个,也就是说我们需要一个二维 dp 数组。
dp[i][w]
的定义如下:对于前 i 个物品,当前背包的容量为 w,这种情况下可以装的最大价值是 dp[i][w]
。
比如说,如果 dp[3][5] = 6
,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。
根据这个定义,我们想求的最终答案就是 dp[N][W]
。base case 就是 dp[0][..] = dp[..][0] = 0
,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
第三步,根据「选择」,思考状态转移的逻辑。
如果你没有把这第 i 个物品装入背包,那么很显然,最大价值 dp[i][w]
应该等于 dp[i-1][w]
,继承之前的结果。
如果你把这第 i 个物品装入了背包,那么 dp[i][w]
应该等于 dp[i-1][w - wt[i-1]] + val[i-1]
。
首先,由于 i 是从 1 开始的,所以 val 和 wt 的索引是 i-1 时表示第 i 个物品的价值和重量。
dp[i-1][w - wt[i-1]]
也很好理解:你如果装了第 i 个物品,就要寻求剩余重量 w - wt[i-1]
限制下的最大价值,加上第 i 个物品的价值 val[i-1]
。
def bag(W, N, wt, val):
dp = [[0] * (W + 1) for _ in range(N + 1)]
for i in range(1, N + 1):
for j in range(1, W + 1):
if j - wt[i - 1] < 0:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j - wt[i - 1]] + val[i - 1], dp[i - 1][j]
return dp[N][W]
问题转化给一个可装载重量为 sum / 2
的背包和 N 个物品,每个物品的重量为 nums[i]
。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
第一步要明确两点,「状态」和「选择」。
状态就是「背包的容量」和「可选择的物品」,
选择就是「装进背包」或者「不装进背包」。
第二步要明确 dp 数组的定义。
dp[i][j] = x
表示,对于前 i 个物品,当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。
比如说,如果 dp[4][9] = true
,其含义为:对于容量为 9 的背包,若只是用钱 4 个物品,可以有一种方法把背包恰好装满。
或者说对于本题,含义是对于给定的集合中,若只对前 4 个数字进行选择,存在一个子集的和可以恰好凑出 9。
根据这个定义,我们想求的最终答案就是 dp[N][sum/2]
,base case 就是 dp[..][0] = true
和 dp[0][..] = false
,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。
第三步,根据「选择」,思考状态转移的逻辑。
如果不把 nums[i]
算入子集,或者说你不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态 dp[i-1][j]
,继承之前的结果。
如果把 nums[i]
算入子集,或者说你把这第 i 个物品装入了背包,那么是否能够恰好装满背包,取决于状态 dp[i - 1][j-nums[i-1]]
。
首先,由于 i 是从 1 开始的,而数组索引是从 0 开始的,所以第 i 个物品的重量应该是 nums[i-1]
,这一点不要搞混。
dp[i - 1][j-nums[i-1]]
也很好理解:你如果装了第 i 个物品,就要看背包的剩余重量 j - nums[i-1]
限制下是否能够被恰好装满。
换句话说,如果 j - nums[i-1]
的重量可以被恰好装满,那么只要把第 i 个物品装进去,也可恰好装满 j 的重量;否则的话,重量 j 肯定是装不满的。
直接上代码
class Solution: def canPartition(self, nums: List[int]) -> bool: n = len(nums) s = sum(nums) if s % 2 != 0: return False s = s // 2 dp = [[False] * (s + 1) for _ 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, s + 1): if j - nums[i - 1] < 0: 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][s]
注意到 dp[i][j]
都是通过上一行 dp[i-1][..]
转移过来的,之前的数据都不会再使用了
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
s = sum(nums)
if s % 2 != 0:
return False
s = s // 2
dp = [False] * (s + 1)
dp[0] = True
for i in range(n):
for j in range(s, -1, -1):
if j - nums[i] >= 0:
dp[j] = dp[j] or dp[j - nums[i]]
return dp[s]
其实这段代码和之前的解法思路完全相同,只在一行 dp 数组上操作,i 每进行一轮迭代,dp[j]
其实就相当于 dp[i-1][j]
,所以只需要一维数组就够用了。
唯一需要注意的是 j 应该从后往前反向遍历,因为每个物品(或者说数字)只能用一次,以免之前的结果影响其他的结果。
至此,子集切割的问题就完全解决了,时间复杂度 O(n*sum),空间复杂度 O(sum)。
class Solution:
def partitionDisjoint(self, A: List[int]) -> int:
left_max = A[0]
curr_max = A[0]
n = len(A)
idx = 0
for i in range(1, n):
curr_max = max(curr_max, A[i])
if A[i] < left_max:
left_max = curr_max
idx = i
return idx + 1
把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
这个问题和我们前面讲过的两个背包问题,有一个最大的区别就是,每个物品的数量是无限的,这也就是传说中的「完全背包问题」
解题思路
第一步要明确两点,「状态」和「选择」。
状态有两个,就是「背包的容量」和「可选择的物品」,
选择就是「装进背包」或者「不装进背包」
第二步要明确 dp 数组的定义。
dp[i][j]
的定义如下:
若只使用前 i
个物品,当背包容量为 j
时,有 dp[i][j]
种方法可以装满背包。
若只使用 coins 中的前 i
个硬币的面值,若想凑出金额 j
,有 dp[i][j]
种凑法。
base case 为 dp[0][..] = 0, dp[..][0] = 1
。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。
我们最终想得到的答案就是 dp[N][amount]
,其中 N 为 coins 数组的大小。
第三步,根据「选择」,思考状态转移的逻辑。
如果你不把这第 i
个物品装入背包,也就是说你不使用 coins[i]
这个面值的硬币,那么凑出面额 j
的方法数 dp[i][j]
应该等于 dp[i-1][j]
,继承之前的结果。
如果你把这第 i
个物品装入了背包,也就是说你使用 coins[i]
这个面值的硬币,那么 dp[i][j]
应该等于 dp[i][j-coins[i-1]]
。
由于 i
是从 1 开始的,所以 coins 的索引是 i-1
时表示第 i
个硬币的面值。
dp[i][j-coins[i-1]]
也不难理解,如果你决定使用这个面值的硬币,那么就应该关注如何凑出金额 j - coins[i-1]
。
比如说,你想用面值为 2 的硬币凑出金额 5,那么如果你知道了凑出金额 3 的方法,再加上一枚面额为 2 的硬币,不就可以凑出 5 了嘛。
综上就是两种选择,而我们想求的 dp[i][j]
是「共有多少种凑法」,所以 dp[i][j]
的值应该是以上两种选择的结果之和:
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0] * (amount + 1) for _ 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] >= 0:
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
else:
dp[i][j] = dp[i - 1][j]
return dp[n][amount]
优化压缩
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [0] * (amount + 1)
dp[0] = 1
for i in range(n):
for j in range(1, amount + 1):
if j - coins[i] >= 0:
dp[j] += dp[j - coins[i]]
return dp[amount]
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
n = len(coins)
for j in range(1, amount + 1):
for i in range(n):
if j - coins[i] >= 0:
dp[j] = min(dp[j], dp[j - coins[i]] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。