本篇文章将站在labuladong文章上对【背包】系列问题进行总结。
通过本文,你可以解决一下leetcode问题。
leetcode 416 分割等和子集
leetcode 518 零钱兑换 ||
动态规划帮助我们有效率地遍历问题的解,反复强调的在做动态规划时,需要注意的点有。
第一,明确【状态】和【选择】
第二,明确dp数组的含义
第三,找出状态转移方程式
第四,确认好边界(base line)情况
一般来说,对于我们比较困难在于第二和第三点,因为两者有密切的练习,我们需要明确含义才能想明白状态之间的改变。
labuladong给出的框架如下:
接下来我们将带着这个框架去解决【背包】系列问题。
0-1背包问题
我们都知道最经典的0-1背包是这样设定的:
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
既然是0-1背包,也就是说物品要么装,要么不装,不可以被分割。
接下来,我们套用标准模板来走一遍思路。
第一步,明确【状态】和【选择】。
【状态】对于我们描述问题是关键,也就是通过状态我们可以有效地表达出当前的问题情况。那么在背包问题中,【背包的容量】和【可选择的物品】就构成了我们的【背包问题】。
对于一个物品我们所拥有的【选择】显而易见,那就是**“放”**还不是“**不放”**这个物品。
第二步 明确dp数组的含义
在上一步中我们只是把关键的要素确定了出来,而在这一步中dp数组起到的就是描述问题的纽带。把我们在步骤一中提到的要素放在dp中,就可以有效地【描述问题】,所以是dp【】={所有状态的总和}。
第三步 根据选择明确状态的转移
对于选择来讲只有两种,放还是不放。那么我们还要对应写出,当物品选择“放”时的【状态】,和不放的状态。
相信大家已经都很熟悉了。
如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。
如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。
第四步 最后不要忘记确认base line。
针对问题,考虑周全它的基本情况。其实在这里我们又可以加深一层体会,在搜寻问题所有解的时候。不管是回溯还是动态规划,本质上也是要找到问题的【突破口】。对于回溯算法,遍历到深处回退,以及动态规划从baseline上开始计算,都需要描述清楚。
附加步骤 状态压缩
当问题的状态比较多时,实际上动态规划求解的过程只需要依靠【上一个状态】做出抉择。基于此,我们常常可以把空间继续压缩,来优化算法,尽管这样会降低代码的可读性。
最后给出0-1背包的代码,懒得打了,来自labuladong,推广好文。
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
0-1背包变体
实际上【背包】问题和子集划分的问题可以相互联系,我们来看实例。
乍一看可能还没那么快反应过来。实际上要求划分的两个子集元素和相等,假设该集合的所有元素和为sum,那么我们就得在集合中找到合适的元素,使得他们的和为sum/2。问题用【背包】语言翻译一下也就是:
给一个背包容量为【sum/2】和【N】个物品(元素),每个物品重量为 nums[i], 是否存在一种装法, 能将背包装满。
根据之前提出的框架和结构,写出代码。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
if(nums.size()==0) return false;
for(int i=0;i<nums.size();i++){
sum+=nums[i];
}
if(sum%2==1) return false;
int target=sum/2;
int n = nums.size();
vector<vector<bool>> dp(n+1,vector<bool>(target+1,false));
for (int i = 0; i <= n; i++)
dp[i][0] = true;
for(int i = 1;i<=n;i++){
for(int j =0;j<=target;j++){
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[n-1][target];
}
};
完全背包问题
完全背包的改变在于物品的使用是不限制的,因此状态的总和来自于【选】与【不选】状态的相加,即是上一个物体不管【选】还是【没选】都不会影响我当前的问题状态,隐藏的含义就是物体的使用是不限制的。
根据框架和步骤,给出代码。
class Solution {
public:
int change(int amount, vector<int>& coins) {
//base line
vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,0));
dp[0][0]=1;
for(int i = 1;i<=coins.size();i++){
for(int j=0;j<=amount;j++){
if(j==0) dp[i][0] = 1;
//if(coins[i]<j) dp[i][j] = 0;
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[coins.size()][amount];
}
};
写在最后
参考: https://mp.weixin.qq.com/s/zGJZpsGVMlk-Vc2PEY4RPw