算法设计与分析/数据结构与算法实验7:0-1背包问题(分支限界法)
视频讲解指路:
https://www.bilibili.com/video/BV1Ah4y1U7oH
1.实验目的
(1)掌握分支限界法的处理思路与算法框架。
(2)掌握应用分支限界法解决具体问题的方法。
(3)掌握分支限界法的广泛应用。
2.实验内容
(1)问题描述
给定
n
n
n种物品和一背包。物品
i
i
i的重量是
w
i
w_i
wi,其价值为
v
i
v_i
vi,背包的容量为
C
C
C。问:应该如何选择装入背包的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品
i
i
i只有两种选择,即装入背包或不装入背包。不能将物品
i
i
i装入背包多次,也不能只装入部分的物品
i
i
i。因此,该问题称为0-1背包问题。
此问题的形式化描述是:给定
C
>
0
,
w
i
>
0
,
v
i
>
0
,
1
≤
i
<
n
C>0,w_i>0,v_i>0,1 \le i <n
C>0,wi>0,vi>0,1≤i<n,要求找出
n
n
n元0-1向量$(x_1,x_2,…,x_n),x_i \in {0,1},
1
≤
i
≤
n
1 \le i \le n
1≤i≤n,使得
∑
i
=
1
n
w
i
x
i
≤
C
\sum_{i=1}^n w_ix_i \le C
∑i=1nwixi≤C,而且
∑
i
=
1
n
v
i
x
i
\sum_{i=1}^n v_ix_i
∑i=1nvixi达到最大。因此,0-1背包问题是一个特殊的整数规划问题。
要求使用分支限界法解决该问题。
(2)输入
输入包含3行。
第一行包含二个数字
n
,
C
n,C
n,C,表示物品个数
n
n
n和背包总体积
C
C
C。
第二行包含
n
n
n个正整数
w
i
w_i
wi,表示每个物品的重量。
第三行包含
n
n
n个正整数
v
i
v_i
vi,表示每个物品的价值。
(3)输出
输出分为两行。
第一行输出使总价值
∑
i
=
1
n
v
i
x
i
\sum_{i=1}^n v_ix_i
∑i=1nvixi达到最大的总价值。
第二行输出物品的选取方案
(
x
1
,
x
2
,
.
.
.
x
n
)
,
x
i
∈
{
0
,
1
}
1
≤
i
≤
n
(x_1,x_2,...x_n),x_i \in \{0,1\} 1\le i \le n
(x1,x2,...xn),xi∈{0,1}1≤i≤n,用“1”表示选取该物品,“0”表示不选该物品。
3.问题实例分析
实例:输入参数
n
=
5
,
C
=
10
,
w
i
=
{
2
,
1
,
3
,
4
,
6
}
,
v
i
=
{
3
,
2
,
4
,
5
,
8
}
n=5,C=10,w_i=\{2,1,3,4,6\},v_i=\{3,2,4,5,8\}
n=5,C=10,wi={2,1,3,4,6},vi={3,2,4,5,8}。
一般情况下,0-1背包问题是NP-hard的。0-1背包问题的解空间可用子集树表示。解0-1背包问题的回溯法与装载问题的回溯法十分类似。在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。设
r
r
r是当前剩余物品价值总和;
c
p
cp
cp是当前价值,
b
e
s
t
p
bestp
bestp是当前最优价值。当
c
p
+
r
≤
b
e
s
t
p
cp+r \le bestp
cp+r≤bestp时,可剪去右子树。计算右子树中解的上界的更好方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直到装不下时,再装入该物品的一部分而装满背包。由此得到的价值是右子树中解的上界。
对于实例中的参数,首先计算每个物品的单位价值。
v
i
/
w
i
=
{
3
2
,
2
,
4
3
,
5
4
,
4
3
}
v_i/w_i=\{\frac32,2,\frac43,\frac54,\frac43\}
vi/wi={23,2,34,45,34}。按物品的单位重量价值递减对物品进行排序并重新编号。得到:
w
i
′
=
{
1
,
2
,
6
,
3
,
4
}
w_i'=\{1,2,6,3,4\}
wi′={1,2,6,3,4},
v
i
′
=
{
2
,
3
,
8
,
4
,
5
}
v_i'=\{2,3,8,4,5\}
vi′={2,3,8,4,5}。此时,
v
i
′
/
w
i
′
=
{
2
,
3
2
,
4
3
,
4
3
,
5
4
}
v_i'/w_i'=\{2,\frac32,\frac43,\frac43,\frac54\}
vi′/wi′={2,23,34,34,45}此时,物品单位重量价值递减。
对于根结点,以拿1和不拿1为依据对根结点进行扩展。拿1时,价值上界为14.33。不拿1时,价值上界为13.67。将这两个结点加入优先队列(堆)。
堆(优先队列)是大根堆,“优先级”即判断函数为价值上界。
此时大根堆堆顶为(“拿1,上界14.33”)。对其进行扩展,以是否拿2作为区分的依据。拿2时,价值上界为14.33。不拿2时,价值上界为14。将这两个结点都加入堆(优先队列)。
此时大根堆堆顶为“拿2,上界14.33”。对其进行扩展,以是否拿3作为区分依据。拿3时,价值上界为14.33。不拿3时,价值上界为14。将这两个结点都加入堆(优先队列)。
此时大根堆堆顶为“拿3,上界14.33”。对其进行扩展,以是否拿4作为区分依据。拿4时,背包此时体积超过上限,不可行。不拿4时,价值上界为14.25。将可行的右结点加入堆(优先队列)。
此时大根堆堆顶为“不拿4,上界14.25”。对其进行扩展,以是否拿5作为区分依据。拿5时,背包此时体积超过上限,不可行。不拿5时,价值上界为13。将可行的右结点加入堆(优先队列)。
此时大根堆堆顶为“不拿3,上界14”。对其进行扩展,以是否拿4作为区分依据。拿4时,价值上界为14。不拿4时,价值上界为10。这两个结点都可行,都加入堆(优先队列)。
此时大根堆堆顶为“拿4,上界14”。对其进行扩展,以是否拿5作为区分依据。拿5时,价值上界为14。不拿5时,价值上界为9。这两个结点都可行,都加入堆(优先队列)。
得到最大上界为14。将该结点的货物选取情况记录,转换为选取货物前的原始编号,得到:
(
x
1
,
x
2
,
x
3
,
x
4
,
x
5
)
=
(
1
,
1
,
1
,
1
,
0
)
(x_1,x_2,x_3,x_4,x_5)=(1,1,1,1,0)
(x1,x2,x3,x4,x5)=(1,1,1,1,0)。
4.算法描述及说明
正如第3节问题实例分析所述,算法的整体流程如下:
1.输入数据,并对每个物品进行编号。
2.计算每个物品的单位价值,并将物品按单位价值排序。
3.计算根结点上界,并将根结点加入大根堆。
4.取出堆顶元素。若堆顶结点为解空间树的叶结点,则算法直接结束,叶结点的物品选取信息和价值为所求的最大价值。
5.若堆顶元素不是叶结点,则尝试扩展左结点。若左结点可行(选取该物品背包体积没有被超出),则对左结点计算上界后入堆。扩展右结点,不选取物品,对右结点计算上界后入堆。
5.算法正确性分析
算法会正确地结束:在利用优先队列遍历解空间后,找到了使总价值最大的解,算法会停止。
分支限界的正确性分析:开始时,根结点是解空间树唯一的活结点,也是当前的扩展结点。算法会不断扩展结点,直到子集树的一个叶结点成为扩展结点时为止。此时优先队列中所有活结点的价值上界不超过该叶结点的价值。因此,该叶结点对应的解为问题最优解。
因此,利用分支限界法会系统地查找背包问题的所有可行解,利用限界函数剪去了不可行的分支,保留了可行并能产生最大解的分支。
从而,该算法是正确的。
6.算法时间复杂性分析
算法计算上界需要
O
(
n
)
O(n)
O(n)时间,在最坏情况下有
O
(
2
n
)
O(2^n)
O(2n)个右儿子结点需要计算上界,故解0-1背包问题的分支限界法所需的计算时间为
O
(
n
2
n
)
O(n2^n)
O(n2n)。
注1:若ppt认为时间复杂度为
O
(
2
n
)
O(2^n)
O(2n),则ppt有误!!!
注2:理论上时间复杂度为
O
(
n
2
n
)
O(n2^n)
O(n2n),但实际上很难达到!!!
7.运行结果展示及其说明
测试样例使用了两组。第一组测试样例为上文问题实例分析中的,正确地解得要拿物品1、物品2、物品3、物品4,且最大总价值为14。同时,随意编一组测试数据.输入
n
=
5
,
C
=
8
,
w
i
=
{
3
,
5
,
1
,
2
,
2
}
,
v
i
=
{
4
,
5
,
2
,
1
,
3
}
n=5,C=8,w_i=\{3,5,1,2,2\},v_i=\{4,5,2,1,3\}
n=5,C=8,wi={3,5,1,2,2},vi={4,5,2,1,3},解得最大总价值为10,拿物品1、物品3、物品4、物品5。
8.心得体会
9.程序源代码
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
double cw;//当前重量
double cp;//当前价值
double bestp;//当前最优价值
int n;//物品数量
double c;//背包容量
const int N = 105;
struct Bag {
double w, v;
int id, x;
};
int bestx[N];
Bag bag[N];
struct BBnode {
BBnode *parent;
bool leftChild;
BBnode(BBnode* par, bool ch) {
parent = par;
leftChild = ch;
}
};
struct heapnode {
BBnode* livenode;
double upperprofit;//价值上界
double profit;//结点价值
double weight;//结点重量
int level;//层序号
bool operator<(const heapnode& b) const {
return this->upperprofit < b.upperprofit;
}
heapnode(BBnode *node,double up, double pp, double ww, int lev) {
livenode = node;
upperprofit = up;
profit = pp;
weight = ww;
level = lev;
}
};
bool cmp(Bag a, Bag b) {
return (a.v / a.w) > (b.v / b.w);
}
bool cmpbound(heapnode a, heapnode b) {
return a.upperprofit < b.upperprofit;
}
priority_queue<heapnode, vector<heapnode>,less<heapnode> > q;
void addlivenode(double up, double pp, double ww, int lev, BBnode* par, bool ch) {
BBnode* b = new BBnode(par, ch);
heapnode node(b, up, pp, ww, lev);
q.push(node);
}
double bound(int i) {
double cleft = c - cw;
double bd = cp;
while (i <= n && bag[i].w <= cleft) {
cleft -= bag[i].w;
bd += bag[i].v;
i++;
}
if (i <= n)
bd += bag[i].v * cleft / bag[i].w;
return bd;
}
void bfs() {
BBnode *enode = NULL;
int i = 1;
bestp = 0;
double up = bound(1);
while (i != n + 1) {
double wt = cw + bag[i].w;
if (wt <= c) {
if (cp + bag[i].v > bestp)
bestp = cp + bag[i].v;
addlivenode(up, cp + bag[i].v, cw + bag[i].w, i + 1, enode, true);
}
up = bound(i + 1);
if (up > bestp)
addlivenode(up, cp, cw, i + 1, enode, false);
heapnode node = q.top();
q.pop();
enode = node.livenode;
cw = node.weight;
cp = node.profit;
up = node.upperprofit;
i = node.level;
}
for (int j = n; j > 0; j--) {
bag[j].x = (enode->leftChild!=NULL) ? 1 : 0;
enode = enode->parent;
}
for (int i = 1; i <= n; i++)
bestx[bag[i].id] = bag[i].x;
return;
}
int main() {
cin >> n >> c;
for (int i = 1; i <= n; i++)
cin >> bag[i].w;
for (int i = 1; i <= n; i++)
cin >> bag[i].v;
for (int i = 1; i <= n; i++)
bag[i].id = i;
sort(bag + 1, bag + 1 + n, cmp);
bfs();
cout << bestp << endl;
for (int i = 1; i <= n; i++)
cout << bestx[i] << " ";
return 0;
}
更多推荐
所有评论(0)