C++数据结构与算法分析——分支限界法(BFS求解01背包问题)

分支限界法介绍

分支限界,又叫分支定界,是一种系统搜索解空间的方法。它是BFS的一种剪枝。
DFS中我们已经知道,在DFS中有隐含的搜索树存在,BFS中同样也有(分析题目时用)
分支限界,分为两个剪枝:分支限界

  1. 分支:即可行性剪枝,当遍历到一个点时,判断它是否符合题意,如果不符合,那么则将这个分支剪去。
  2. 限界:即最优性剪枝,这需要一个估价函数来判断当前遍历到的点是否有可能得到最优解,如果不可能得到最优解,那么该分支也可剪去。

这么说也许有些抽象,当前我们选择01背包作为例子:

例题

题目描述

N件物品和一个容量是V的背包。每件物品只能使用一次。

i件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大
输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品数量和背包容积。
接下来有N行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

样例解释与算法设计

首先我们知道有4件物品,并且最大容量5
v 1 = 1 , w 1 = 2 v_1 = 1,w_1 = 2 v1=1,w1=2
v 2 = 2 , w 2 = 4 v_2 = 2,w_2 = 4 v2=2,w2=4
v 3 = 3 , w 3 = 4 v_3 = 3,w_3 = 4 v3=3,w3=4
v 4 = 4 , w 4 = 5 v_4 = 4,w_4 = 5 v4=4,w4=5
本题我们用瞪眼法可以看出选第二件物品和第三件物品可以得到最大价值为 w 2 + w 3 = 8 w_2 + w_3 = 8 w2+w3=8(瞪眼法不确定可以手动算一下),那么如何来完成这个选择呢?
之前已经讲过遗传算法求解01背包动态规划(DP),分析01背包的性质,我们可以发现,它的每个物品能用且只能用一次。而在DP中我们也说了,遍历到第i个物品时,我们可以将之分为两个状态,即不选,那么因此,我们就可以用1和0来代表选或不选从而得到一个解空间:
解空间

这是BFS的一棵搜索树。
其中根节点是一个初始点,它不代表任何选项,而它分支出来的两个节点,左子树表示的是不选择第一个物品的方案,右子树表示的是选择第一个物品的方案,以此类推,在对第四个点做完方案选择之后,会得到一组解空间:第一个解表示4个物品都不选,第二个解表示只选择第4个物品……。

常规做法

在正常情况下,我们会用BFS一个一个解求出来,然后将总体积超过背包容量 V V V的解删去,然后在剩下的解中找到一个总价值最大的解,这样写的时间复杂度固定为 O ( 2 n ) O(2^n) O(2n),时间复杂度过于庞大,因此我们会考虑能否对这个算法进行一些优化呢?

可行性剪枝(分支)

首先我们从常规做法中可以发现,在求完所有解后需要对总体积超过背包容量 V V V的解删去,那么我们能否在BFS过程中提前将这些解剪枝掉呢?答案是可以的。我们只需要在每个方案中维护一个当前总体积即可,如果当前总体积超过了背包容量 V V V的话,我们就可以将以该方案为根节点的整棵子树剪枝,也容易证明:
假设当前方案体积 v t > V v_t > V vt>V,那么之后所有选择中,只有对下件物品的选或不选两个选择,那么它的子树的总体积 v t s o n > = v t > V v_{tson} >= v_t > V vtson>=vt>V恒成立,即可将其剪枝。
可行性剪枝

经过可行性剪枝之后,以上框住的部分都将会被剪枝。为什么它叫可行性剪枝呢,因为它筛选的是连最简单的背包容量都会超过的选择方案,无论它的价值如何,我们都无法选择该方案,因为装不进背包,即该方案不可行。那么还能不能进一步剪枝呢?

最优性剪枝(限界)

最优性剪枝,表示在最优化问题中,若当前花费的代价已经超过了当前搜索的最优解,那么它无论如何都无法比当前最优解更优,可以直接停止对当前分支的搜索,即对当前方案树剪枝。
限界,顾名思义,就是给定一个估价函数和一个界限,一旦该方案的估价函数值 > 界限值,那么将停止对当前分支的搜索,对当前方案树剪枝。
那么如何设置估价函数呢?
估价函数设计思路不唯一,但是要设计一个快准狠的估价函数却也并不简单,我们通过题目意思可以得到这样的信息:题目需要我们求的是价值最大的方案(假设已经进行过可行性剪枝,不存在超容量的选择方案,即每个方案都是合法的)。因此我们可以:

  1. 设计一个估价函数界限值res
  2. 在每个节点中都存一个值c表示当前方案的总价值,一个r表示之后可以获得的最大价值(剩余总价值),剩余总价值表示的是后面所有物品的价值之和,比如当前遍历第一件物品,那么它的剩余总价值就是后面三件物品的总价值。

每遍历到一个点idx,我们都算一下它的r + c值,即当前总价值与剩余总价值之和,将之与当前最优解进行比较,如果r + c <= res,即意味着就算之后的物品它都选都无法超过最优解,即可将之剪枝,否则将它加入队列进行BFS。
如果遍历到了解空间,那必然说明当前解的价值c > res,更新res。(图画不下了就不配图了)
如此又可以剪枝一些方案树。

在最优性剪枝的基础上我们能否继续进行优化呢?

优化搜索顺序

从最优性剪枝中,我们可以发现每个方案树的节点的估价函数值其实是不一样的,这就又带来了一个思考:在合法范围内,估价函数值越大的节点往下搜索得到最优解的可能应该是越大的,那么我们能否通过一些手段来优化搜索顺序呢?即让估价函数值越大的节点越提前搜索。其实是可行的。我们知道BFS是通过队列这个数据结构来完成的,它会一层一层往下拓展,那么我们只需要将队列改成优先队列,让估价函数值越大的节点入队后自动排到靠前位置,即可优化搜索顺序。

代码

#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;

const int N = (1 << 20) + 10;
int n,m;
int v[N],w[N];
int res = -1;
int s[N]; // s[i]存储的是从第1件物品到第i件物品的价值总和 

struct good{
    int idx,c,r,tv; // idx表示选法下标,c表示该选法的当前总价值,r表示当前选法的剩余总价值,tv表示该选法的当前总体积
    bool operator > (const good& W) const{
        return W.c + W.r > c + r;
    }
}goods[N];

int bfs(){
    goods[1] = {1,0,0,0};
    priority_queue<good,vector<good>,greater<good>> q;
    q.push(goods[1]);

    while(q.size()){
        auto t = q.top();
//        cout << t.idx << endl;
        q.pop();
        int idx = t.idx << 1;
        goods[idx] = {idx,goods[t.idx].c,s[n] - s[(int)log2(idx)],goods[t.idx].tv};
        goods[idx + 1] = {idx + 1,goods[t.idx].c + w[(int)log2(idx)],s[n] - s[(int)log2(idx)],goods[t.idx].tv + v[(int)log2(idx)]};
        if((int)log2(t.idx) == n) { // 假如已经是子节点,则更新答案
            res = max(res,t.c);
            continue;
        }

        if(goods[idx].tv <= m && goods[idx].c + goods[idx].r > res) q.push(goods[idx]); // 假如当前选法的总体积不超过背包容量,且当前价值+剩余价值 > 当前最优解,则装入背包
        if(goods[idx + 1].tv <= m && goods[idx + 1].c + goods[idx + 1].r > res) q.push(goods[idx + 1]);
    }

    return res;
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i],s[i] = s[i - 1] + w[i];
//
//    for(int i = 2; i < 1 << n + 1; i ++){
//        goods[i] = {i,goods[i >> 1].c + (i&1)*w[(int)log2(i)],s[n] - s[(int)log2(i)],goods[i >> 1].tv + (i&1)*v[(int)log2(i)]};
//    }
//
//    for(int i = 1; i < 1 << n + 1; i ++) printf("i = %d,c[i] = %d,r[i] = %d,tv[i] = %d\n",goods[i].idx,goods[i].c,goods[i].r,goods[i].tv);
    cout << bfs() << endl;
    return 0;
}

运行截图

截图

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L_Hygen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值