0-1背包--回溯法求最优值和最优解问题

  • 问题描述

有n个重量分别为w1,w2,…,wn的物品(物品编号为1~n),它们的价值分别为v1,v2,…,vn,给定一个容量为W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么选不中,要求选中的物品不仅能够放到背包中,而且具有最大价值,并对下表所示3个物品求出W=20时的所有解和最佳解。

物品编号

重量

价值

1

10

20

2

5

15

3

15

25

  • 基本思路

  1. 回溯法基本思路

确定了解空间的组织结构后,回溯法从开始节点(根节点)出发,以深度优先搜索整个解空间。这个开始的节点为活节点,同时成为当前的扩展节点。在当前的扩展节点处,搜素向纵深方向移至一个新节点。这个新节点就成为新的活节点,并成为当前扩展节点。如果当前节点处不能再向纵深方向移动,则当前扩展节点为死节点。此时,应往回移动到最近的一个活节点处。回溯法以这种方式递归的在解空间中搜素,直至找到所有符合要求的解或解空间中已无活节点。(即深度优先搜素)

【优化方法】

剪枝(一):当前决策放入的物品总重量已经大于背包容量时,没有必要继续决策,此时可以将其左子树剪掉。

剪枝(二):如果将当前扩展节点后剩下的所有物品都装入还没有目前已求得的最优值大的话,就不在进行决策了,直接返回。

递归回溯时,在当前扩展节点处会通过设置约束函数和限界函数。不满足条件的,剪去相应的子树

  1. 实例

例:

假设:

物品个数为 n=3

背包的容量为 C=20

物品的重量分别为 w={10,5,15}

物品的价值分别为 v={2015,25}

此时的解空间可以为(x1, x2, x3)的所有可能取{(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)}

遍历解空间(深度优先)

 

从根往下遍历时,要求每次经过节点都计算此时是否满足容量的条件,当某个分支可以满足重量要求时,记录它的价值总量,以便最后选择最好的价值量。

例如第一次遍历时,经过x1=1,x2=1时,此时重量为15第三个就放不进去了,那么我们使用剪枝算法的约束函数剪去x3=1这个分支,并遍历x3=0这个分支,再从x2=0往下拓展x3=1或者0的情况。其余节点都是按照此类方法依次辨别是否能放进背包中。

代码:

#include<iostream>
using namespace std;

#define M 105
int n;//表示物品个数
int W;//表示背包总载重量
double w[M];//表示对应物品重量
double v[M];//表示对应物品价值
double x[M];//用来判断是否是否放入背包
double zw;//当前放进背包总重量
double zv;//当前放进背包总价值
double bestv; //当前最优值
double bestx[M]; //当前最优解

double Bound(int i) //计算上界
{
    int rp = 0;
    while (i <= n) //计算剩余物品重量和
    {
        rp += v[i];
        i++;
    }
    return zv + rp;
}

void Backtrack(int t) //回溯函数
{
    if (t > n)//已经到达了叶子结点
    {
        for (int j = 1; j <= n; j++)
        {
            bestx[j] = x[j]; //记录编号
        }
        bestv = zv;//保留当前最优值
        return;
    }

    if (zw + w[t] <= W)//如果满足条件,扩展并且搜索左子树
    {
        x[t] = 1;
        zw += w[t];
        zv += v[t];
        Backtrack(t + 1);//回溯
        zw -= w[t];
        zv -= v[t];
    }

    if (Bound(t + 1) > bestv)//如果满足限界条件,搜索右子树
    {
        x[t] = 0;
        Backtrack(t + 1);//回溯
    }
}

//计算最优解
void Knapsack(double W, int n)
{
    //初始化
    zw = 0;
    zv = 0;
    bestv = 0;
    double sumw = 0;
    double sumv = 0;
    for (int i = 1; i <= n; i++)
    {
        sumw += w[i];
        sumv += v[i];
    }
    //如果背包够大都能放进去,那最大价值就是全部都进去
    if (sumw <= W)
    {
        bestv = sumv;
        cout << "放进背包的物品最大价值是:" << bestv << endl;
        return;
    }

    Backtrack(1);//回溯

    cout << "放入背包的物品最大价值是:" << bestv << endl;
    cout << "放进背包的物品的序号是:";
    for (int i = 1; i <= n; i++)
    {
        if (bestx[i] == 1)
        {
            cout << i << " ";
        }
    }
    cout << endl;
}

int main()
{
    cout << "请输入物品的个数:" ;
    cin >> n;
    cout << "请输入背包的重量W:";
    cin >> W;
 
    cout << "请依次输入每个物品的重量w,用空格分开: ";
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i];
    }

    cout << "请依次输入前面所输入的物品每个所对应的的价值v,用空格分开: ";
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i];
    }

    cout << endl;
    Knapsack(W, n);
    return 0;
}

结果:

 

  • 心得体会

本次实验了解了用回溯法求解0-1背包问题的最优值和最优解,分别通过笔算和用代码实现了回溯法求解0-1背包的具体计算过程。

遇到的问题如下

  1. 在实验中对于剪枝优化的代码不知道怎么去实现

根据深度优先遍历先去搜索左子树,并把所有左子树的最优值找到。通过函数调用,在当前节点的时候去计算后面右节点的所有后面层数的价值总数有没有超过已经计算过的最优值,如果在这一节点他的右子树后面的所有值加起来会比当前节点所有的左子树的最优值还大,那么就可以执行右子树的遍历否则直接剪掉右子树的遍历。通过规定上界函数解决剪枝问题,问题得到解决。

  1. 在求最优解时如何打印出最优解的位置选择?

在实验中我在计算每一层节点是否要选择的时候都是给X[t]数组来赋值0或1以用来表示是否选择该节点放进背包,当遍历到叶子节点时,我就会把这一条遍历线路上计算的最优值保存然后把每一层的节点的选择情况保存到另一个数组中,当后面有更好的最优值的时候,这些数据都会被覆盖替代。到最后通过for循环将取值为1的输出出来即可,下标的情况就是层数的情况,即可知道节点的选择情况如何了,问题得到解决。

回溯法解0_1背包问题时,会用到状态空间树。在搜索状态空间树时,只要其左儿子结点是一个可行结点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。设r是当前剩余物品价总和;cp是当前价;bestp是当前最优。当cp+r≤bestp时,可剪去右子树。计算右子树中解的上界可以用的方法是将剩余物品依其单位重量排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包。由此得到的价是右子树中解的上界,用此来剪枝。 为了便于计算上界,可先将物品依其单位重量从大到小排序,此后只要顺序考察各物品即可。在实现时,由MaxBoundary函数计算当前结点处的上界。它是类Knap的私有成员。Knap的其他成员记录了解空间树的节点信息,以减少函数参数的传递以及递归调用时所需要的栈空间。在解空间树的当前扩展结点处,仅当要进入右子树时才计算上界函数MaxBoundary,以判断是否可以将右子树减去。进入左子树时不需要计算上界,因为其上界与父结点的上界相同。 在调用函数Knapsack之前,需要先将各物品依其单位重量从达到小排序。为此目的,我们定义了类Objiect。其中,运算符与通常的定义相反,其目的是为了方便调用已有的排序算法。在通常情况下,排序算法将待排序元素从小到大排序。 在搜索状态空间树时,由函数Backtrack控制。在函数中是利用递归调用的方法实现了空间树的搜索
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值