作为动态规划的典型问题,0-1背包思路适配的地方很多,网上很多版本,个人最能看明白的是carl的代码随想录,里面从二维到一维到dp数组,起码讲得还算能应付做题,毕竟算法这个东西,有的人需要很明白,有的人就稍微背背就行。
问题本身
首先我们要知道的是:0-1背包问题本身。
有N个不同重量的物品分别拥有重量weight[N],它们的价值分别为value[N]
现在给你一个背包,背包容量---即能装下总重量为bagSize的物品,问这个背包能装的最大价值。
首先我们能确定的是:对于每一个背包重量,最大价值是唯一的。
其次需要我们意识到问题服务的对象是:容量大小为X的背包里的最大价值
dp[]:
背包容量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
最大价值 |
物品重量 | A | B | C | D | E | F | G | H |
物品价值 | a | b | c | d | e | f | g | h |
我们把第一个表格定义为dp数组,其大小就是背包容量,我们希望根据顺序的规则,对数组操作,并将结果“容量大小为X的背包里的最大价值”记录在数组的最后一个元素。
那么背包问题遵从什么规则呢?
由于是从前往后的顺序构造,也就是dp[i]比dp[j](i < j)要早得到。
那这个求dp[N]问题是不是转换成:
已知dp[0]~dp[N-1],求dp[N]?
观察这个过程: 当有一个物品重量为A,价值为a,当前有一个背包,大小为N;已知当背包大小为N-A(N-A<N)时的最大价值为V1-------------->大小为N的背包的价值(把A装进来的情况)就是a+V1。
如果有很多物品,则需要遍历这些物品,然后取得最大值,就是背包大小为N的最大价值。
因此将遍历过程写成代码:(结合文字看才有感觉)
for(int i = 0;i < 物品个数;i++){//这一层是遍历所有物品
for(int j = 最大重量;j >= 物品重量;j--){//这一层遍历的是不同物品的价值
dp[j] = Max(dp[j],dp[j-物品重量]+物品价值)
}
}
其中dp[j]本身会有一个初始值,这个初始值需要稍微根据题目来思考下。
当会写出一维数组的情况时,那就不要担心二维的了,二维的更直观简单。
一道例题
这道题不难,考验的是问题的转化能力,怎么把这个符号求和问题转化为0-1背包问题?
1:不同的符号不要紧,只要确定了一堆是正数,那另一堆一定是负数,只考虑正数X就可以解题
2: 有固定的目标和,由加减关系:X-(sum-X)=target 得到所有正数的和X
3.问题转化为:在所有数(物品)中找出加和为X(背包大小)的方案数量(价值)。
好了,个人觉得可以贴代码了
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//先对数组中的元素求和
int sum = 0;
for(int i = 0;i < nums.length;i++)sum+=nums[i];
//这是一些边界
if(target>0&&sum<target)return 0;
if(target<0&&sum<-target) return 0;
if((target+sum)%2==1)return 0;
//根据求和信息和target的关系,将数组分为正负两部分
//也就可以求出取正数的那部分为size
int size = (sum+target)/2;
//将问题转化为在nums中取出任意个数,使它们的和为size
//这就是一个典型的0-1背包问题
if(size<0)size = -size;
int []dp = new int[size+1];
//dp[j]表示总和为j的元素组合方案个数为dp[j]
dp[0] = 1;
//需要遍历的是整个nums,以作为滚动数组
//本质的含义是将二维的dp数组铺平展开,逐层累加
for(int i = 0;i < nums.length;i++){
for(int j = size;j>=nums[i];j--){
dp[j] =dp[j-nums[i]]+dp[j];
}
}
return dp[size];
}
}