给定n个重量为w1,w2,w3,…,wn,价值为v1,v2,v3,…,vn的物品和容量为C的背包
求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大
注意:0-1背包问题指的是每个物品只能使用一次
示例:
输入:
int[] weight = {2,5,3,4};
int[] value = {10,7,4,6};
int c = 10;
输出:21
解释:重量上限c=10,最大能装重量为2、5、3的三件物品,价值为10+7+4=21
首先我们先来了解一下回溯法:
是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。——来自百度百科
其实我觉得这次百度的解释还挺好理解的,如果还是有点不理解,那我在这里再举个例子:
(这张图是国产悬疑游戏《完美的一天》里的地图,截图来自B站up渗透之C君的实况视频,这里仅用来举例说明回溯法,若有问题及时删除)
这里我们一开始假设在振华西站
,最终目的地是要到达百姓银行
,限时1个小时,到各节点的时间是:
振华西站
→盘旋路
:20分钟
盘旋路
→小西湖
:20分钟 盘旋路
→石排巷
:10分钟
小西湖
→百姓银行
:20分钟 石排巷
→百姓银行
:25分钟
当然我们一看就知道怎么走是最快捷的,但程序不知道啊,程序就要去试。
先尝试振华西站
→盘旋路
→小西湖
→百姓银行
=60分钟。程序不知道好不好,只知道这个满足要求,那就先存下来;
再尝试振华西站
→盘旋路
→石排巷
→百姓银行
=55分钟。嗯,这个更快,保存这个。
这时我们发现,其实振华西站
→盘旋路
这段20分钟的路是完全一样的,所以程序在执行完第一次后只需要回到盘旋路
,接着执行下一次的就行了,这里这个回到盘旋路
的操作就是回溯
,这样的方法就是回溯算法。
其实程序还会执行那条更远的路,但很明显不可能满足限时条件,所以便舍弃,这就是回溯算法的优化——剪枝
(ps.其实就是懒得写)
然后我们回归这道题,在0-1背包问题里,每个物品只有两种情况,放入背包 和 不放入背包,这样我们就可以使用1
和0
来表示,然后对没个物品进行判断,便得到了一棵树:
起始点: x
/ \
物品1: 1 0
/ \ / \
物品2: 1 0 1 0
/\ /\ /\ /\
物品3:1 0 1 0 1 0 1 0
我们假设从起始点x
开始,用回溯法遍历所有可能,那就是先x111
,然后回溯一位再执行x110
,再回溯执行xx101
……其实有点像树的遍历思想,每一个都尝试一遍。
但是宝友儿,这可不兴算啊!这样尝试下来计算量太大了,还很慢,那我们可以优化一下:
在每一次把新物品放入背包时,加一个限定条件,当前剩余的空间够放这个新的物品时,我们才放进来,不然的话便舍弃这条枝干。形象点说,这就是剪枝
。
if (nowSumWeight + weight[node] <= c)
好了,主题讲完了,可以开始尝试了!
注意:回溯的时候一定要把之前放入背包的东西拿出来,不然这算哪门子的回溯,无限套娃呢隔这儿??
先看上面的分析!自己理解思考一下,不要急着看代码!
//定义属性
static int maxValue = 0; //最大价格
static int nowSumValue = 0; //当前放入背包的物品总价格
static int nowSumWeight = 0; //当前放入背包的物品总重量
//测试方法
@Test
public void test(){
int[] weight = {2,5,3,4};
int[] value = {10,7,4,6};
int c = 10;
backtrack(0,weight,value,c);//不能放入sout,不然返回的不是最优解
System.out.println(maxValue);//输出maxValue才是最终答案
}
//算法主体
public int backtrack(int node, int[] weight, int[] value, int c){
int num = weight.length;
int[] flag = new int[num];//是否放入背包的标志位,1为放,0为不放
Arrays.fill(flag, 0);//将flag数组清空
if (node > num - 1){
//是最低一级的叶子节点
if (maxValue < nowSumValue) maxValue = nowSumValue;
return maxValue;//返回最大价值
}else {
//不是最低一层的子节点,那就进行遍历
//每一个物品只有两种情况,1为放入背包,0为不放入背包
for (int i = 0; i < 1; i++) {
flag[node] = i;//把两种情况分别放在flag数组里
switch (i){
case 0: backtrack(node + 1, weight, value, c);//不放入背包,就直接判断下一个节点
case 1: {
//放入背包,判断是否放的进去
if (nowSumWeight + weight[node] <= c){
//放入背包,加入总价和总重
nowSumWeight += weight[node];
nowSumValue += value[node];
//判断下一个节点
backtrack(node + 1, weight, value, c);
//重点!!!
//执行到这里时证明子节点下的节点已经判断完毕,需要回溯,就需要把这一层节点装入背包的东西拿出来,恢复重量与价格
nowSumWeight -= weight[node];
nowSumValue -= value[node];
}// end if
}// end case1
}// end switch
}// end for
}
return 0;
}