算法练习——0-1背包问题(回溯法)

0-1背包问题(回溯法)


题目:

给定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,最大能装重量为253的三件物品,价值为10+7+4=21

分析:

首先我们先来了解一下回溯法

回溯法(探索与回溯法)

是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。——来自百度百科

其实我觉得这次百度的解释还挺好理解的,如果还是有点不理解,那我在这里再举个例子:
回溯法举例图
(这张图是国产悬疑游戏《完美的一天》里的地图,截图来自B站up渗透之C君的实况视频,这里仅用来举例说明回溯法,若有问题及时删除)

这里我们一开始假设在振华西站,最终目的地是要到达百姓银行,限时1个小时,到各节点的时间是:
                        振华西站盘旋路:20分钟
 盘旋路小西湖:20分钟       盘旋路石排巷:10分钟
小西湖百姓银行:20分钟     石排巷百姓银行:25分钟
路径时间
当然我们一看就知道怎么走是最快捷的,但程序不知道啊,程序就要去试。
先尝试振华西站盘旋路小西湖百姓银行=60分钟。程序不知道好不好,只知道这个满足要求,那就先存下来;
再尝试振华西站盘旋路石排巷百姓银行=55分钟。嗯,这个更快,保存这个。

这时我们发现,其实振华西站盘旋路这段20分钟的路是完全一样的,所以程序在执行完第一次后只需要回到盘旋路,接着执行下一次的就行了,这里这个回到盘旋路的操作就是回溯,这样的方法就是回溯算法。
其实程序还会执行那条更远的路,但很明显不可能满足限时条件,所以便舍弃,这就是回溯算法的优化——剪枝
(ps.其实就是懒得写)

题目解析:

然后我们回归这道题,在0-1背包问题里,每个物品只有两种情况,放入背包不放入背包,这样我们就可以使用10来表示,然后对没个物品进行判断,便得到了一棵树:

起始点:        x        
           /       \
物品11        0
         / \       / \
物品21    0    1    0
       /\   /\   /\   /\
物品31 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;
    }
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值