01背包问题-队列分支限界法-C++

0-1背包问题-队列分支限界法

问题描述:

给定n种物品和一个背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?对于给定的n种物品的重量和价值,以及背包的容量,计算可装入背包的最大价值。

算法设计过程:

考虑使用队列分支限界法来解决来解决0-1背包问题,对于每一个物品,我们都有选或者不选两种情况,因此该问题的解空间为一颗子集树。

要采用队列来求解该问题,对于队列中的每一个节点,都要维护当前节点所选择的物品集合,同时维护当前节点的总价值和容量,为剪去不可行解和优化搜索过程做准备。

对于该问题,我们在使用队列分支限界的搜索过程,在处理当前节点时,可以考虑对左右儿子进行约束和限界:
对于该节点的左儿子,我们要考虑其是否满足约束,具体的,我们要考虑当前背包剩余空间能否装下这个物品,也就是当前的总重加上该物品的总量要小于背包总容量时我们才考虑进入左儿子节点。否则左儿子就是一个不可行解,直接跳过即可。
对于该节点的右儿子,我们考虑设计一个限界函数bound来限制进入右儿子的条件,在搜索过程我们维护一个当前的最优解,我们要通过计算进入右子树后有没有可能得到更优的解,只有右儿子包含最优解时我们才选择进入。具体的,设cv是当前的价值,我们考虑将该物品跳过后的最优价值为r,对于当前的最优解bstv,若是\cv+r<=bstv时,我们便考虑剪去右子树,为了更好的实现,我们需要在搜索之前对物品按单位重量价值进行排序,然后依次装入物品,直到装不下时,再装入该物品的一部分而装满背包,由此得到右子树的上界bound。

对于该问题我们的具体算法流程:
1.对所有物品按单位重量价值从大到小进行排序。
2.初始化队列,加入初始的节点,这里以第一个物品作为根节点加入。
3.当队列不为空时,每次从队列中取出一个节点的进行扩展。
4.如果左儿子是可行节点,即满足约束条件时,如果该结点的价值大于最优解,则更新最优解,同时将该节点加入队列。
5.对于扩展节点的右儿子,当其满足限界函数时才将其加队列中。
6.当队列为空时,说明搜索过程已经结束,直接返回最优解即可。

流程图:
在这里插入图片描述

代码:

#include <bits/stdc++.h>
using namespace std;

struct Obj{ // 物品
    int id; 
    int w;
    int val;
    bool operator<(Obj &obj){
        return val * obj.w > obj.val * w;
    }
};

struct Node {
    int dep; // 深度,第几层就是处理第几个物品
    int cv; /// 当前价值
    int cw; // 当前容量
    vector<int>x; // 解向量
    
    Node(int dep,int cv, int cw, vector<int>x):dep(dep), cv(cv), cw(cw), x(x) {} 

    friend ostream& operator<<(ostream& os, const Node& p){
        cout << "dep:" << p.dep << " cv:" << p.cv << " cw:" << p.cw;
        cout << " x:";
        for(int i:p.x)cout << i << ' ';
        return os;
    };  

};

struct Whopxx{
    int n; // 物品数量
    vector<Obj>vt; // 物品
    int sum; // 背包大小

    vector<int>bstx; // 最优解
    int bstv;
    
    Whopxx(int n,int sum, vector<int>w, vector<int>p):n(n), sum(sum) {
        bstx.resize(n + 1);
        bstv = 0;
        vt.resize(n + 1);
        for(int i = 1; i <= n; i++){
            vt[i] = {i, w[i], p[i]};
        }
        sort(vt.begin() + 1, vt.end()); // 按单价从大到小排序
    }

    void work(){
        queue<Node> q;
        q.push(Node(1, 0, 0, {})); // 第一个物品开始做选择
        while(q.size()){
            Node node = q.front(); q.pop();
            cout << node << " bound:" << Bound(node) << " bstv:" << bstv << endl;
            int i = node.dep;
            if(i > n || node.cw == sum){ // 到达叶子结点或背包已满
                if(node.cv > bstv) update(node); // 更新最大值
                continue;
            }
            
            auto [id, w, val] = vt[i];
            if(node.cw + w <= sum){ // 左儿子满足约束
                Node lnode = add(node, vt[i]);
                q.push(lnode);
                if(lnode.cv > bstv) update(lnode);
            }

            if(Bound(node) > 1.0 * bstv){ // 右儿子满足限界
                Node rnode = uadd(node);
                q.push(rnode);
            }
            
        }
    }

    double Bound(Node node){ // 限界函数
        int rw = sum - node.cw; // 剩余容量
        int i = node.dep + 1;
        double b = node.cv;
        while(i <= n && vt[i].w <= rw){
            rw -= vt[i].w;
            b += vt[i].val;
            i++;
        }
        if(i <= n){
            b += vt[i].val * 1.0 / vt[i].w * rw;
        }
        return b;
    }

    Node add(Node node, Obj obj){
        node.cv += obj.val;
        node.cw  += obj.w;
        node.x.push_back(obj.id);
        node.dep += 1;
        return node;
    }

    Node uadd(Node node){
        node.dep += 1;
        return node;
    }

    void update(Node node){
        bstx = node.x;
        bstv = node.cv;
    }
};


int main(){ 
    freopen("input.txt","r", stdin);
    freopen("output.txt", "w", stdout);
    
    int n, c; // 物品数量,背包容量
    cin >> n >> c;
    vector<int>w(n + 1);
    vector<int>p(n + 1);
    for(int i = 1; i <= n; i++) cin >> p[i];
    for(int i = 1; i <= n; i++) cin >> w[i];

    Whopxx wx(n, c, w, p);
    wx.work();
    vector<int>ans(n + 1);
    for(int i:wx.bstx) ans[i] = 1;
    cout << wx.bstv << endl;
    for(int i = 1; i <= n; i++){
        cout << ans[i] << ' ';
    }

    fclose(stdin);
    fclose(stdout);
    return 0;
}
/*
4 7
9 10 7 4
3 5 2 1


3 30
45 25 25
16 15 15

4 15
10 10 12 18
2 4 6 9

5 10
2 2 6 5 4
6 3 5 4 6


4 10
40 42 25 12
4 7 5 3
*/

实验测试结果及分析:
测试数据:
input.txt
在这里插入图片描述

通过运行程序得到:
output.txt
在这里插入图片描述

在该输出结果中,cv是当前节点的物品价值总和,cw是当前总重,bound是进入右子树后的最大值,bstv是进行到当前节点的全局最优解得值,形象的可以用如下图来表示该搜索过程:
在这里插入图片描述

最终我们得到最优解即选择物品1和物品3装入背包,得到的最优总价值为65。

复杂度分析:限界函数的时间复杂度为O(n),在最坏的情况下有2^n个右儿子结点需要计算上界,所以计算需要的最坏情况下的时间复杂度为O(n2 ^n)。

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用C++实现优先队列分支限界法求解0-1背包问题的代码: ```c++ #include <iostream> #include <queue> #include <algorithm> using namespace std; struct Node { int level; // 当前节点所在的层数 int profit; // 当前节点的价值 int weight; // 当前节点的重量 double bound; // 当前节点的价值上界 }; bool operator<(const Node& a, const Node& b) { return a.bound < b.bound; } double bound(Node u, int n, int W, int* p, int* w) { if (u.weight >= W) { return 0; } double bound = u.profit; int j = u.level + 1; int totweight = u.weight; while ((j < n) && (totweight + w[j] <= W)) { totweight += w[j]; bound += p[j]; j++; } if (j < n) { bound += (W - totweight) * p[j] / w[j]; } return bound; } int knapsack(int n, int* p, int* w, int W) { priority_queue<Node> Q; Node u, v; u.level = -1; u.profit = 0; u.weight = 0; u.bound = bound(u, n, W, p, w); int maxprofit = 0; Q.push(u); while (!Q.empty()) { u = Q.top(); Q.pop(); if (u.bound > maxprofit) { v.level = u.level + 1; v.weight = u.weight + w[v.level]; v.profit = u.profit + p[v.level]; if (v.weight <= W && v.profit > maxprofit) { maxprofit = v.profit; } v.bound = bound(v, n, W, p, w); if (v.bound > maxprofit) { Q.push(v); } v.weight = u.weight; v.profit = u.profit; v.bound = bound(v, n, W, p, w); if (v.bound > maxprofit) { Q.push(v); } } } return maxprofit; } int main() { int n = 5; int p[] = {3, 2, 4, 1, 5}; int w[] = {2, 1, 3, 2, 4}; int W = 7; int maxprofit = knapsack(n, p, w, W); cout << "Max profit: " << maxprofit << endl; return 0; } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值