背包问题
主要是0-1背包和完全背包
重点在于边界条件(也就是一个都没有选的时候,初始应该是怎样的)和转移方程
背包九讲
代表前i个物品放入一个大小为j的背包获取到的最大价值 ,就有
由上述状态转移方程可知, 的值只与 有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)
下面是不降维写法
01背包
416. 分割等和子集
边界条件:没有任何元素可以选取时,可以得到的和为0,因此
转移方程:
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total & 1:
return False
target = total//2
n = len(nums)
dp = [[False] * (target+1) for _ in range(n+1)]
dp[0][0] = True
for i in range(1, n+1):
for j in range(target+1):
if j >= nums[i-1]:
dp[i][j] = dp[i-1][j-nums[i-1]] | dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]
474. 一和零
边界条件:没有任何元素可以选取时,可以得到的字符串数量只能是0,因此时,
转移方程:
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
l = len(strs)
dp = [[[0]*(n+1) for _ in range(m+1)] for _ in range(l+1)]
for k in range(1,l+1): # l个元素依次增加
for i in range(m+1):
for j in range(n+1):
cnt0 = strs[k-1].count('0')
cnt1 = strs[k-1].count('1')
if cnt0 <= i and cnt1 <= j:
dp[k][i][j] = max(dp[k-1][i][j], dp[k-1][i-cnt0][j-cnt1]+1)
else:
dp[k][i][j] = dp[k-1][i][j]
return dp[-1][-1][-1]
494. 目标和
边界条件:没有任何元素可以选取时,元素和只能为0,因此边界条件为,或者
转移方程:这里需要用加号表示选或不选两种情况的数目之和
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if (total+abs(target)) & 1 == 1:
return 0
aim = (total+abs(target))//2
n = len(nums)
dp = [[0]*(aim+1) for _ in range(n+1)]
# dp[i][j] 用i个数能达到j的组合数量
dp[0][0] = 1
for i in range(1, n+1):
for j in range(aim+1):
if j >= nums[i-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]
879. 盈利计划
注意:下面的代码会超时
过两天再写一遍不超时的(0302),也就是改成至少达到利润k
定义:是前i项工作,恰好使用上j个人,恰好能达到利润k
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
m = len(group) # 工作数
maxProf = sum(profit) # 最大可能的利润
dp = [[[0] * (maxProf+1) for _ in range(n+1)] for _ in range(m+1)]
# 边界条件 这里的定义是“恰好用上j个人”,所以只有dp[i][0][0]才为1
# for i in range(m+1):
# for j in range(n+1):
# dp[i][j][0] = 1 # 要改成dp[i][0][0]才是正确的
dp[0][0][0] = 1
for i in range(1, m+1):
for j in range(n+1):
for k in range(maxProf+1):
if k >= profit[i-1] and j >= group[i-1]:
dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-group[i-1]][k-profit[i-1]]
else:
dp[i][j][k] = dp[i-1][j][k]
res = 0
for j in range(n+1):
for k in range(minProfit, maxProf+1):
res += dp[-1][j][k]
return res
完全背包
322. 零钱兑换
边界条件: 达到金额为0最少用0枚硬币
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = len(coins)
dp = [[float('inf')]*(amount+1) for _ in range(n+1)]
dp[0][0] = 0
# for i in range(n+1):
# dp[i][0] = 0
for i in range(1, n+1):
for j in range(amount+1):
if j >= coins[i-1]:
dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i-1]]+1)
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1] if dp[-1][-1] != float('inf') else -1
518. 零钱兑换 II
含义:为使用前i种硬币能达到金额为j的组合总数
边界条件: 不用硬币能达到金额为0的一种
转移方程:
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0] * (amount+1) for _ in range(n+1)]
dp[0][0] = 1
for i in range(1,n+1):
for j in range(amount+1):
if j >= coins[i-1]:
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]
对比
494(01背包)和518(完全背包)的状态转移方程
会发现,它们的区别在于后面的是还是
但其实,完全背包的 还可以进一步改变
可以看出来,下面的比起上面的多了一个,也就是数量不仅限于一个。
或者,换一种理解
在01背包中,求时,用到的是上一轮求得的,所以肯定只能被选一次。
而在完全背包中,求时,用到的是在本轮中之前就已经更新的值,已经更新意味着已经被选取过。
1449. 数位成本和为目标值的最大数字
含义: 前个数字达到时使用数字次数最多时的情况
自己写的代码如下,有很多可以改进的地方:
- 初始化时,设置为
float('-inf')
,最后在判断时,看cnt
是否大于0能知道是否能拼凑出target
,就可以省略if dp[i][j-cost[i-1]]['cnt'] != -1 and
的判断 - 使用一个
from
数组来记录状态,而不是用这个['list']
还有空间优化,具体看官方题解
class Solution:
def largestNumber(self, cost: List[int], target: int) -> str:
n = 9
dp = [[0]*(target+1) for _ in range(n+1)]
for i in range(n+1):
for j in range(target+1):
dp[i][j] = {
'cnt': -1,
'list': [0]*(n+1),
}
# 边界条件
dp[0][0]['cnt'] = 0
for i in range(1, n+1):
for j in range(target+1):
if j >= cost[i-1]:
if dp[i][j-cost[i-1]]['cnt'] != -1 and dp[i][j-cost[i-1]]['cnt']+1 >= dp[i-1][j]['cnt']:
dp[i][j]['cnt'] = dp[i][j-cost[i-1]]['cnt']+1
dp[i][j]['list'] = dp[i][j-cost[i-1]]['list'].copy()
dp[i][j]['list'][i] += 1
else:
dp[i][j]['cnt'] = dp[i-1][j]['cnt']
dp[i][j]['list'] = dp[i-1][j]['list'].copy()
else:
dp[i][j]['cnt'] = dp[i-1][j]['cnt']
dp[i][j]['list'] = dp[i-1][j]['list'].copy()
res = ""
if dp[-1][-1]['cnt'] == -1:
return "0"
else:
for i in range(9,0,-1):
for _ in range(dp[-1][-1]['list'][i]):
res += str(i)
return res