编程语言
首页 > 编程语言> > 算法设计与分析/数据结构与算法实验6:0-1背包问题(回溯法)

算法设计与分析/数据结构与算法实验6:0-1背包问题(回溯法)

作者:互联网

目录

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=1n​wi​xi​≤C,而且 ∑ i = 1 n v i x i \sum_{i=1}^n v_ix_i ∑i=1n​vi​xi​达到最大。因此,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=1n​vi​xi​达到最大的总价值。
第二行输出物品的选取方案 ( 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​}此时,物品单位重量价值递减。在装入物品时,也先装入单位重量价值最大的物品。
因此,先装入第一个物品,此时体积足够装入第二个物品。装完第二个物品后,还能在装第三个物品。以深度优先的顺序,此时访问到的解空间树的结点如图所示:
注意:我自己都觉得图太丑了,大家画图时不要为了方便用wps自带的画图工具画!!!还是visio好!!!
在这里插入图片描述
此时,剩余体积为1.利用约束函数 ∑ i = 1 n w i x i ≤ C \sum_{i=1}^n w_ix_i \le C ∑i=1n​wi​xi​≤C,即物品总重量不能超过背包容量,判断得出不能再装入物品4或5,即左子树不能再继续生长。记录此时取得的最大价值就是当前价值, b e s t p = c p = 13 bestp=cp=13 bestp=cp=13。将物品3撤出背包,即回溯到上一结点,并考虑生长右子树。
在这里插入图片描述
此时, c p = 5 cp=5 cp=5当前剩余物品价值总和为 r = 9 r=9 r=9。 c p + r > b e s t p cp+r>bestp cp+r>bestp,需要考虑这个右子树而不能剪枝、限界将右子树减去。
此时,可以拿下物品4。拿完物品4后,仍然剩余了足够的体积拿物品5。此时,剩余体积0,不能继续放入任何其他物品。而且在这一叶子结点中,所有物品都已经被遍历完成了。记录当前的总价值为 c p = 14 cp=14 cp=14,并将最大价值更新为 b e s t p = c p = 14 bestp=cp=14 bestp=cp=14。
在这里插入图片描述
从该结点回溯到上一结点,并在访问右子树前分别计算每个结点的当前价值 c p cp cp与剩余价值 r r r,发现都可以将右子树直接剪枝剪掉。
在这里插入图片描述
进一步回溯,回溯到考虑是否拿物品2的结点。此时, c p = 2 cp=2 cp=2。对于不拿物品2的情况,先装入重量为6价值为8的物品3,后装入重量为3价值为4的物品4。此时,背包体积不足以装入下一个货物。可以算出此时剩余物品价值总和上界 r = 12 r=12 r=12,满足 c p + r ≤ b e s t p cp+r \le bestp cp+r≤bestp,也可以直接将该右子树剪去,如下图所示。
在这里插入图片描述
接下来,还能回溯到根结点root,不拿物品1。对于物品2、物品3、物品4、物品5,拿完完整的物品2和物品3后价值为11,体积为8。剩余体积2,可以拿2/3个物品4,价值为8/3。此时,物品总价值上界为13.67.即: b o u n d = r = 13.67 < 14 bound=r=13.67<14 bound=r=13.67<14。可以直接将这一分支剪去。
在这里插入图片描述
在计算完最大价值后,还需要求解取得最大价值的方案。因为在算法开始时需要对物品按单位价值重新排序并重新编号,所以在算法开始时的排序前还需要记录物品的新编号与原编号的对应关系。
找到取得 b e s t p = c p = 14 bestp=cp=14 bestp=cp=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.对于第 i i i个物品,判断剩余体积是否能够装下该物品。
4.若能装下该物品,则将该物品装入,并构造相应结点。
5.若不能装入该物品,则不将该物品装入,考虑下一物品。
6.在第4步中的物品撤出背包,构造相应结点,并计算剩余物品能产生价值的上界。若上界大于当前最优解,则装入考虑下一物品,否则不考虑。
7.重复3-6的步骤,直到所有物品都被考虑过。
在这里插入图片描述

5.算法正确性分析

算法会正确地结束:在遍历完解空间后,找到了使总价值最大的解,算法会停止。
回溯法的正确性分析:开始时,根结点是解空间树唯一的活结点,也是当前的扩展结点。在这个扩展结点处,搜索向深度方向移至一个新节点。这个新结点成为新的扩展结点。如果在当前扩展结点处不能再向纵深方向移动,则当前扩展结点成为死结点。应回溯至最近的活结点处,使得这个活结点成为当前扩展结点。回溯法以系统的方式递归地在解空间树中进行搜索,直到找到所要求的解惑解空间中已无活结点。
因此,利用回溯法会系统地查找背包问题的所有可行解,在剪枝时利用限界函数与剪枝函数剪去了不可行的分支,保留了可行并能产生最大解的分支。
从而,该算法是正确的。

6.算法时间复杂性分析

算法计算上界需要 O ( n ) O(n) O(n)时间,在最坏情况下有 O ( 2 n ) O(2^n) O(2n)个右儿子结点需要计算上界,故解0-1背包问题的回溯算法backtrack所需的计算时间为 O ( n 2 n ) O(n2^n) O(n2n)。(注:若ppt上认为该算法时间复杂度为 O ( 2 n ) O(2^n) O(2n),则ppt有误!!!

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>
using namespace std;
double cw;//当前重量
double cp;//当前价值
double bestp;//当前最优价值
int n;//物品数量
double c;//背包容量
const int N = 105;
int x[N];
struct Bag {
	double w, v;
	int id, x;
};
Bag bag[N];
bool cmp(Bag a, Bag b) {
	return (a.v / a.w) > (b.v / b.w);
}
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 dfs(int i) {
	if (i > n) {
		bestp = cp;
		for (int i = 1; i <= n; i++) 
				x[bag[i].id] = bag[i].x;
		return;
	}
	if (cw + bag[i].w <= c) {
		cw += bag[i].w;
		cp += bag[i].v;
		bag[i].x = 1;
		dfs(i + 1);
		cw -= bag[i].w;
		cp -= bag[i].v;
		bag[i].x = 0;
	}
	if (bound(i + 1) > bestp)
		dfs(i + 1);
}
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);
	dfs(1);
	cout << bestp<<endl;
	for (int i = 1; i <= n; i++)
		cout << x[i] << " ";
	return 0;
}

标签:结点,cp,装入,bestp,算法,背包,回溯,物品,数据结构
来源: https://blog.csdn.net/qq_46640863/article/details/122655826