文章目录
文章目录
前言
跟着labuladong刷题,做一些笔记,原网址链接: link.
1. 经典动态规划:0-1 背包问题
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
第一步:明确【状态】和【选择】
状态用于描述子问题
例如在这个例子中,状态由两部分构成, 背包的剩余容量, 以及可以被选择的物品
选择是子问题之间的连接
例如在这个例子,选择就是对某个物品装进背包,或者不装进背包
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
第二步:明确dp数组的定义
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 dp 数组。
dp[i][w] 的定义如下:对于前 i 个物品,当前背包的容量为 w.
这种情况下可以装的最大价值是 dp[i][w]。
比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。
base case 就是 dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
int[][] dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 装进背包,
不把物品 i 装进背包
)
return dp[N][W]
第三步: 根据「选择」,思考状态转移的逻辑。
上面伪码中「把物品 i 装进背包」和「不把物品 i 装进背包」怎么用代码体现出来呢?
这就要结合对 dp 数组的定义,看看这两种选择会对状态产生什么影响:
如果你没有把这第 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 个物品的价值和重量。
伪代码:
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - wt[i-1]] + val[i-1]
)
return dp[N][W]
2. 经典动态规划:子集背包问题
416.分割等和子集(中等)
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
可以把问题转化成0,1背包问题
用nums数组加和的一半当成背包容量,看能不能用一个子集正好凑出这个结果
状态:剩余容量, 可选择物品
选择:装还是不装
dp[i][w] = ture 表示可以选择i个物品,剩余w容量的情况下,能够正好装满
basecase: dp[0][…] = false dp[…][0] = true
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s %2 != 0:
return False
weight = int(s/2)
n = len(nums)
dp = [[False] * (weight + 1) for _ in range(n + 1)]
#dp[n][weight]
#base case, 剩余容量是0的时候不用装就是满的
for i in range(n+1):
dp[i][0] = True
for i in range(1, n + 1):
for j in range(1, weight + 1):
if j >= nums[i-1]:
#注意这里两个都是i-1
dp[i][j] = dp[i-1][j-nums[i-1]] or dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] #不够大不能装第i个
return dp[n][weight]
状态压缩
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s %2 != 0:
return False
weight = int(s/2)
n = len(nums)
dp = [False] * (weight + 1)
#dp[n][weight]
#base case, 剩余容量是0的时候不用装就是满的
dp[0] = True
for i in range(1, n + 1):
for j in reversed(range(1, weight + 1)):
if j >= nums[i-1]:
dp[j] = dp[j-nums[i-1]] or dp[j]
return dp[weight]
注意这里for j in reversed(range(1, weight + 1)):
是从后往前遍历,因为 dp[j-nums[i-1]] 其实应改是上一行的数据 dp[i-1][j-nums[i-1]]
通过从后往前遍历,当计算并更改 dp[j] 的时候并没有更改 dp[j-nums[i-1]] ,所以 dp[j-nums[i-1]] 仍然可以当作上一行的数据来使用
3. 经典动态规划:完全背包问题
518.零钱兑换II(中等)
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
完全背包问题就是每种物品的数量可以是无限的
状态:剩余容量, 可选择物品
选择:当前装几个
dp数组定义:dp[weight][i] = res 表示剩余容量是weight,可选择前 i个 物品时,组合的总数
初始化:dp[weight][i] 都为0
basecase:dp[0][…] = 1 表示weight是0的时候,不论怎么选择都有恰好一种组合, 也就是啥也不选
选择:装几个
装0个:dp[weight][i] = dp[weight][i-1]
装1个:dp[weight][i] = dp[weight - coins[i-1]][i-1]
装2个:dp[weight][i] = dp[weight - 2 * coins[i-1]][i-1]
…
直到weight - 2 * coins[i-1]<0
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
weight = amount
dp = [[0] * (n+1) for _ in range(weight + 1)]
dp[0] = [1] * (n + 1)#basecase: when weight = 0
for w in range(1, weight + 1):
for i in range(1, n + 1):
k = 0
while w >= k * coins[i-1]:
dp[w][i] += dp[w - k * coins[i-1]][i-1]
k += 1
return dp[weight][n]
这种方法非常慢, 原因是我们两个循环里面还有一个while循环,还是计算了不少重复子问题
其实可以稍微改良一下
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
weight = amount
dp = [[0] * (n+1) for _ in range(weight + 1)]
dp[0] = [1] * (n + 1)#basecase: when weight = 0
for w in range(1, weight + 1):
for i in range(1, n + 1):
if w >= coins[i-1]:
dp[w][i] = dp[w - coins[i-1]][i] + dp[w][i-1]
else:
dp[w][i] =dp[w][i-1]
return dp[weight][n]
在压缩一下空间
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
weight = amount
dp = [0] * (weight + 1)
dp[0] = 1
for i in range(1, n + 1):
for w in range(1, weight + 1):
if w >= coins[i-1]:
dp[w] = dp[w - coins[i-1]] + dp[w]
return dp[weight]
这里注意要把i 和 w的遍历调换一下顺序