【回溯法】--01背包问题

回溯法

回溯法是一种非常有效的方法,有“通用的解题法”之称。它有点像穷举法,但是更带有跳跃性和系统性,他可以系统性的搜索一个问题的所有的解和任一解。回溯法采用的是深度优先策略。
回溯法在确定了解空间的结构后,从根结点出发,以深度优先的方式搜索整个解空间,此时根结点成为一个活结点,并且成为当前的扩展结点。每次都从扩展结点向纵向搜索新的结点,当算法搜索到了解空间的任一结点,先判断该结点是否肯定不包含问题的解(是否还能或者还有必要继续往下搜索),如果确定不包含问题的解,就逐层回溯;否则,进入子树,继续按照深度优先的策略进行搜索。当回溯到根结点时,说明搜索结束了,此时已经得到了一系列的解,根据需要选择其中的一个或者多个解即可。
回溯法解决问题一般分为三个步骤;
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间。

算法分析

【整体思路】

01背包属于找最优解问题,用回溯法需要构造解的子集树。对于每一个物品i,对于该物品只有选与不选2个决策,总共有n个物品,可以顺序依次考虑每个物品,这样就形成了一棵解空间树: 基本思想就是遍历这棵树,以枚举所有情况,最后进行判断,如果重量不超过背包容量,且价值最大的话,该方案就是最后的答案。

在搜索状态空间树时,只要左子节点是可一个可行结点,搜索就进入其左子树。对于右子树时,先计算上界函数,以判断是否将其减去(剪枝)。

上界函数bound():当前价值cw+剩余容量可容纳的最大价值<=当前最优价值bestp。

为了更好地计算和运用上界函数剪枝,选择先将物品按照其单位重量价值从大到小排序,此后就按照顺序考虑各个物品。
在这里插入图片描述
那么问题的解空间就很明显了就是2的8次方个种问题的解的组合{(0,0,0,0,0,0,0,0)…(1,1,1,1,1,1,1,1)}。
采用深度优先策略进行搜索,搜索的过程如下所示;
在这里插入图片描述
搜索得到的最终的结果就是159;
搜索过程分析(可以对照代码阅读)
从第一个点开始向下扩展,扩展到将第四个点放入放入后,此时背包已经放入重量为56,价值为96。然后继续扩展,将第五个物品放入重量为89,价值为139。此时第六个物品无法继续放入,但是i=5的,也就是没有到达最后一个点,此时调用Bound()函数算出可能获得的最大的价值为164.66,那么继续向下扩展,但是无法放入物品了,继续调用Bound()判断是否向下扩展,直到最后一个物品也无法放入,确定此时搜索得到的可以获得的最大的价值为139,保存物品放入的选择。
然后开始回溯,回溯遇到第一个放入背包的物品对应的节点,将其取0(把物品拿出背包,进入右子树)。此时比较背包剩余重量,发现第六个物品可以放入背包,放入后背包重量为99,价值为149。但是,第七个物品无法继续放入,那么调用Bound(),发现可能获得162的价值,大于139,继续向下扩展,直到最后一个物品也无法放入,确定此时搜索得到的可以获得的最大的价值为149>139,更新可获得的价值,并且保存物品放入的选择。
继续回溯,直到回溯到根结点。此时的最大价值所对应的物品放入选择就是所求的解。

#include <iostream>
#include <stdio.h>
using namespace std;
int n;//物品数量
double c;//背包容量
double v[100];//各个物品的价值 value
double w[100];//各个物品的重量 weight
double cw = 0.0;//当前背包重量 
double cp = 0.0;//当前背包中物品总价值 
double bestp = 0.0;//当前最优价值
double perp[100];//单位物品价值(排序后)
int order[100];//物品编号
int put[100];//设置是否装入,为1的时候表示选择该组数据装入,为0的表示不选择该组数据 记录当前最优解
int x[100];//记录从根到当前结点的路径

//按单位价值排序
void knapsack()
{
    int i,j;
    int temporder = 0;
    double temp = 0.0;
    for(i=1;i<=n;i++)
        perp[i]=v[i]/w[i]; //计算单位价值(单位重量的物品价值)默认重量不为0
    for(i=1;i<=n-1;i++)//也可以将其封装成类 则只需用 sort函数排序perp数组,就可以省略3个数组的交换赋值
        for(j=1;j<=n-i;j++)
            if(perp[j]<perp[j+1])//冒泡排序perp[] (order[],v[],w[]跟随perp[]变化)
            {
                temp = perp[j];  //冒泡对perp[]排序
                perp[j]=perp[j+1];
                perp[j+1]=temp;

                temporder=order[j];// 保存物品编号
                order[j]=order[j+1];
                order[j+1]=temporder;

                temp = v[j];
                v[j]=v[j+1];
                v[j+1]=temp;

                temp=w[j];
                w[j]=w[j+1];
                w[j+1]=temp;
            }
}
//回溯函数
void backtrack(int i) //i用来指示到达的层数(第i步,从1开始),同时也指出当前选择了几个物品
{   
    double bound(int i);//定义一个上界函数
    if(i>n) //递归结束的判定条件 到达叶节点 n+1层
    {       //只有更优解才能进入叶节点
        for(int j=1;j<=n;j++)
            put[j]=x[j];//搜索到叶节点处(更优解)就修正put[]的值
        bestp = cp;
        return;
    }
    //如若左子节点可行,则直接搜索左子树;
    //对于右子树,先计算上界函数,以判断是否将其剪去
    if(cw+w[i]<=c)//将物品i放入背包,搜索左子树
    {
        cw+=w[i];//同步更新当前背包的重量
        cp+=v[i];//同步更新当前背包的总价值
        x[i]=1;//记录当前路径
        backtrack(i+1);//深度搜索进入下一层
        cw-=w[i];//回溯复原
        cp-=v[i];//回溯复原
        x[i]=0;//回溯复原
    }

    if(bound(i+1)>bestp){
            //如若符合条件则搜索右子树
        backtrack(i+1);
    }
}

//计算上界函数,功能为剪枝
double bound(int i)
{   //判断当前背包的总价值cp+剩余容量可容纳的最大价值<=当前最优价值
    double leftw= c-cw;//剩余背包容量
    double b = cp;//记录当前背包的总价值cp,最后求上界
    //以物品单位重量价值递减次序装入物品
    while(i<=n && w[i]<=leftw)
    {
        leftw-=w[i];
        b+=v[i];
        i++;
    }
    //装满背包
    if(i<=n)
        b+=v[i]*leftw/w[i];
    return b;//返回计算出的上界

}

int main()
{
    int i;
    printf("请输入物品的数量和背包的容量:");
    scanf("%d %lf",&n,&c);
    for(i=1;i<=n;i++){
        scanf("%lf",&w[i]);
        scanf("%lf",&v[i]);
        order[i]=i;//保存物品编号
    }
    knapsack();
    backtrack(1);
    printf("最优价值为:%lf\n",bestp);
    printf("需要装入的物品编号和质量分别是:\n");
    for(i=1;i<=n;i++)
    {
        if(put[i]==1){
            printf("%d ",order[i]);
            cout<<v[i]<<endl;
        }
    }
    return 0;
}


  • 6
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值