1、基础问题:
给你一个可以装载重量为w的背包和n个物品,每个物品有价值和重量2个属性,其中第i个物品重量wt[i],价值为val[i],现在用这个背包装物品,限定重量下最多价值是多少
动态规划标准套路:
1、确定状态和选择
状态:背包重量和可选择的物品
选择:装进背包和不装进背包
2、dp数组定义:
dp[i][j] 对于前i个物品,当背包重量为j时的最大价值时dp[i][j]
初始状态:
dp[0][……]: 没有物品时为0
dp[……][0]: 背包没有空间时能装的价值是0
返回的最终解:
dp[N][W]
3、状态转移矩阵
dp[i][j]=
1、if 没有把物品放进背包:dp[i-1][j] 维持之前结果
2、if 把物品放进背包:max(dp[i-1][j], dp[i-1][j-wt[i-1]]+val[i-1])
val和wt的索引是从1开始的,所以这里wt val用i-1代表第i个物品的状态
模板
dp[N+1][W+1]
dp[0][……] = 0
dp[……][0] = 0
for i in [1...N]:
for j in [1...W]:
把物品i装进背包
不把物品i装进背包
return dp[N][W]
实际解法:
def knapsack(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] = max(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1])
else:
dp[i][j] = dp[i-1][j]
return dp[n][w]
也可以将wt,val补第一个数据为0,拉齐下标:
def knapsack(w,n,wt,val):
dp = [[0]*(w+1) for _ in range(n+1)]
wt.insert(0,0)
val.insert(0,0)
print(dp)
print(wt,val)
for i in range(1,n+1):
for j in range(1,w+1):
if j - wt[i] < 0:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i-1][j-wt[i]] + val[i],dp[i-1][j])
return dp[n][w]
状态压缩(这里是遍历,不是全排列,为了避免重复内循环使用逆序)
(状态压缩的分类详解,可以参考动态规划状态压缩详解https://blog.csdn.net/weixin_43298886/article/details/110152066 )
def knapsack(w,n,wt,val):
dp = [0 for _ in range(w+1)]
for i in range(0,n):
for j in range(w,-1,-1):
if j - wt[i] >= 0:
dp[j] = max(dp[j],dp[j-wt[i]]+val[i])
return dp[w]
2、进阶:子集背包问题
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
可以分解成sum(nums)//2的问题
基础版
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total %2 == 1:return False
total = total // 2
n = len(nums)
dp = [[False]*(total+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,total+1):
if j - nums[i-1] < 0:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]|dp[i-1][j-nums[i-1]]
return dp[n][total]
基础版代码类似
在这个基础上进行状态压缩
状态压缩(内循环逆序避免重复遍历)
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total %2 == 1:return False
total = total // 2
n = len(nums)
dp = [False for _ in range(total+1)]
dp[0] = True
#待优化
for i in range(n):
for j in range(total,-1,-1):
if j - nums[i] >=0:
dp[j] =dp[j] | dp[j-nums[i]]
return dp[total]
更近一步,j的遍历可以进一步优化
j-nums[i] >=0 的判定可以放在for j in range(total,num-1,-1)进行,这个循环中j 必然大于或者=num
备注:for j in range(total,num-1,-1)取值范围为[num,total]
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total %2 == 1:return False
total = total // 2
n = len(nums)
dp = [False for _ in range(total+1)]
dp[0] = True
for i,num in enumerate(nums):
for j in range(total,num-1,-1):
dp[j] =dp[j] | dp[j-num]
return dp[total]
3、完全背包问题
难度中等600收藏分享切换为英文接收动态反馈
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
a、基础状态转移矩阵:
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]:#coins 和 dp的起始索引不一样,coins[i-1]代表第i个元素,
#dp[i-1]代表第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[n][amount]
b、状态压缩(全排列,允许重复遍历)
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [1] + [0 for _ in range(amount)]
for i in range(n):
for j in range(1,amount+1):
if j >= coins[i]:
dp[j] = dp[j]+dp[j-coins[i]]
return dp[amount]