0-1背包:有n个物品,第i个物品的体积为w[i],价值为v[i],每个物品至多选择一个,求体积和不超过capacity的最大价值和。
对于第i个物品,我们只有两种选择:选,或者不选。如果选,那么背包的剩余容量减少w[i].如果不选,那么背包的剩余容量不变。
而问题实际上是:在剩余容量为c时,从前i个物品中得到的最大价值和为多少?
根据分析,我们可以进一步拆分这个问题。如果不选,那么子问题就为:在剩余容量为c时,从前i-1个物品中得到的最大价值和为多少?如果选,那么子问题就为:在剩余容量为c-w[i]时,从前i-1个物品中得到的最大价值和为多少?
显然可以用动态规划了。
第一步:确定dp数组的意思:dp[i][j]表示:从第0-i种物品中挑选,放进容量为j的背包里,得到的最大价值和。
第二步:初始化dp数组(对边界情况进行处理):当背包容量为0时,无法装下物品,因此价值为0,由此可令dp[i][0]==0;当选择物品的范围为0-0时,意味着我们只能选第一个物品,那么最大价值和只能为v[0],由此可令dp[0][i]==v[0].
第三步:填充。由上可知,要么选,要么不选。但是我们不能忘记选择权从何而来:背包的剩余容量大于等于物品体积。如果不是,那么直接不选。如果是,那么就根据前面的分析进一步处理。如果不选则dp[i][j]=dp[i-1][j].如果选,那么dp[i][j]=dp[i][j-w[i]]+v[i].选择二者中的最大值即可。
int backpack01(int kinds, int capacity, vector<int>& w, vector<int>& v)
{//kinds表示物品数量,capacity表示背包总容量,w[i]表示第i+1个物品的体积,v[i]表示第i+1个物品的价值vector<vector<int>>dp(kinds, vector<int>(capacity + 1, 0));//初始化://1.背包容量为0时,无法装下物品,最大价值和为0for (int i = 0; i < kinds; i++){dp[i][0] = 0;}//2.只有一种物品时,只能选择第一个物品,最大价值和为v[0]for (int i = 0; i <= capacity; i++){dp[0][i] = v[0];}//填充for (int i = 1; i < kinds; i++)//物品编号{for (int j = 1; j <= capacity; j++)//背包剩余容积{if (j < w[i]) dp[i][j] = dp[i - 1][j];//容不下当前物品,只能不选else{dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);}}}return dp[kinds - 1][capacity];
}
我们发现,dp[i][?]只与dp[i-1][?]有关,因此我们可以对空间复杂度进行优化,用一个一维数组dp滚动处理即可。
int backpack01(int kinds, int capacity, vector<int>& w, vector<int>& v)
{vector<int>dp(capacity + 1, 0);//dp[i]表示:背包容量为i时里面物品的最大价值和dp[0] = 0;//背包容量为0,装不下物品,最大价值和为0for (int i = 0; i < w.size(); i++)//外层循环:控制某一种物品{for (int j = capacity; j >= w[i]; j--)//内层循环:对于该种物品进行处理{dp[j] = max(dp[j], dp[j - w[i]] + v[i]);}}return dp[capacity];
}
关键点:j从capacity开始倒着处理。为什么要倒着处理?实际上,我们可以用“双数组”的理念去理解(双数组滚动也可以,只不过单数组空间复杂度更优,用双数组滚动更容易想象)。我们退化成两个数组,数组a用来保存上一轮的dp值,数组b用来填充接下来的dp值。前面我们说过,dp[i][x]实际上只由dp[i-1][x]决定,所以填充b只需要依赖a中的值。但也正因如此,我们在填充b的时候,就不能改变a中的值了。我们现在只有一个数组,如果我们从前往后处理,那么相当于:我们在填充b中dp值时,它所依赖的a中的dp值已经被改变了。所以我们需要从后往前处理,因为填充依赖前面的原始数据。两个关键词:前面的+原始。
同时我们还进行了一点优化:j>=w[i].这意味着我们不需要再判断当前背包的容量是否能够装下物品了,因为这时候物品已经定下来了(外层循环控制)。所以小于w[i]的j必然都装不下,就不用处理了。
看一道具体的题目:
2915. 和为目标值的最长子序列的长度 - 力扣(LeetCode)
本题中:下标相当于物品编号,下标对应的值相当于物品的体积,target相当于背包的容量,长度相当于价值。
int lengthOfLongestSubsequence(vector<int>& nums, int target)
{if (accumulate(nums.begin(), nums.end(), 0) < target) return -1;int kinds = nums.size(), capacity = target;vector<int>dp(capacity + 1, -1);dp[0] = 0;for (int i = 0; i < kinds; i++){for (int j = capacity; j >= nums[i]; j--){if (dp[j - nums[i]] != -1)//注意这里{dp[j] = max(dp[j], dp[j - nums[i]] + 1);}}}return dp[capacity];
}
为什么需要判断dp[j-nums[i]]是否等于-1?
我们先来想想等于-1意味着什么。dp[j-nums[i]]==-1意味着:在考虑前i个元素时,无法组成和为j-nums[i]的子序列。而只有当存在j-nums[i]的子序列时,我们才能通过添加当前元素nums[i]来得到和为j的子序列。
因此,如果dp[j-nums[i]]等于-1,那意味着没有基础来构建dp[j],所以不能更新。