问题描述
- 给定n种物品和一个背包。物品i的重量是wi,其价值为vi,背包的容量为c。
- 应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
- 在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。
算法描述
0-1背包问题是子集选取问题。一般情况下,0-1背包问题是NP难得。0-1背包问题的解空间可用子集树 表示。在搜索解空间的时,只要其左儿子节点是一个可行节点,搜索就进去其左子树(约束条件)。当右子树中可能包含最优解时才进入右子树搜索(限界函数)。否则就将右子树剪去。
计算右子树中解的上界的更好方法是将剩余物品依其单位重量价值排序
,然后依次装入物品,直至装不下时,再装入物品的一部分
而装满背包。由此得到的价值是右子树中解的上界
。
为了便于计算上界,可先将物品按照单位重量价值由大到小排序,此后,只要按照顺序考察各个物品即可
。
在实现时,由Bound计算当前结点处的上界。在解空间树的当前扩展结点处,仅当要进入右子树时才计算右子树的上界Bound
,以判断是否将右子树剪去。
进入左子树时不需要计算上界
,因为其上界与其父节点上界相同。
计算步骤
- 计算每种物品单位重量的价值si=pi/wi
- 依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。
- 若将这种物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包。
- 依此策略一直地进行下去,直到背包装满为止。
例子
问题描述
假设有4个物品,物品的价值分别为p=[9, 10, 7, 4], 重量分别为w=[3, 5, 2, 1], 背包容量C=9,使用回溯方法求解此0-1背包问题,计算其最优值及最优解。要求画出求得最优解的解空间树, 给出具体求解过程。要求中间被剪掉的结点用×标记。
求解步骤
按照单位重量价值由大到小排好序,按照顺序考查是否装入物品
物品单位重量价值:[p1/w1, p2/w2, p3/w3, p4/w4] =(3, 2, 3.5, 4)
按照物品单位重量价值由大到小排序:
物品 | 重量(w) | 价值(v) | 价值/重量(v/w) |
---|---|---|---|
1 | 1 | 4 | 4 |
2 | 2 | 7 | 3.5 |
3 | 3 | 9 | 3 |
4 | 5 | 10 | 2 |
上界计算
先装入物品1,然后装入物品2 和 3。装入这三个物品后,剩余的背包容量为3,只能装入物品4的3/5(2 * 3/5 = 1
)。 即上界为4+7+9+2*(9-6)=26
深度优先搜索解空间树,若左儿子是可行节点,才搜索进入左子树,否则将左子树剪掉;若右子树包含最优解,才搜索右子树,否则将右子树剪掉。
以第一个up=26为例,26=4+7+9+2*(9-6)
打x的部分因为up值已经小于等于Bestp了,所以没必要继续递归了。
样例分析演示
算法
#include <bits/stdc++.h>
using namespace std;
class Object
{
public:
int p; //价值
int w; //重量
int id; //物品的id
};
//物品数量 背包容量 当前价值 当前重量 最优值 总重量 总价值
int n, c, cp, cw, bestp, total_w, total_p;
int x[100], best_x[100]; //记录暂时选取状态 记录最优选取状态
Object O[100]; //存储物品的数组
void Input()
{
cout<<"请输入物品个数和背包容量:";
cin>>n>>c;
cout<<"请输入每个物品的价值和重量:"<<endl;
for(int i = 1; i <= n; ++i)
{
cin>>O[i].p>>O[i].w;
O[i].id = i;
}
}
bool cmp(const Object &a, const Object &b)
{
return a.p / a.w > b.p / b.w;
}
void Initilize()
{
cp = cw = total_w = total_p = bestp = 0;
for(int i = 1; i <= n; ++i)
{
total_p += O[i].p;
total_w += O[i].w;
}
//依照物品单位重量价值排序
sort(O + 1, O + n + 1, cmp);
//for(int i = 1; i <= n; ++i)
//cout << O[i].id << endl;
}
//计算up值
int Bound(int i)
{
int temp_cw = c - cw;
int temp_cp = cp;
while(i <= n && temp_cw >= O[i].w)
{
temp_cw -= O[i].w;
temp_cp += O[i].p;
++i;
}
//装满背包
if(i <= n)
temp_cp += O[i].p / O[i].w * temp_cw;
return temp_cp;
}
void Backtrack(int i)
{
if(i > n)
{
bestp = cp;
for(int i = 1; i <= n; ++i)
best_x[i] = x[i];
return;
}
if(cw + O[i].w <= c)
{
x[O[i].id] = 1;
cw += O[i].w;
cp += O[i].p;
Backtrack(i + 1);
cp -= O[i].p;
cw -= O[i].w;
x[O[i].id] = 0;
}
//向右求解的约束条件
if(Bound(i + 1) > bestp)
{
x[O[i].id] = 0;
Backtrack(i + 1);
}
}
void OutPut()
{
cout<<"bestp = "<<bestp<<endl;;
cout<<"选取的物品为 : ";
for(int i = 1; i <= n; ++i)
{
if(best_x[i] == 1)
cout << i << " ";
}
}
int main()
{
Input();
Initilize();
Backtrack(1);
OutPut();
}
测试用例
输入
请输入物品个数和背包容量:4 9
请输入每个物品的价值和重量:
9 3
10 5
7 2
4 1
输出
bestp = 23
选取的物品为 :
1 2 4
因为输入的数据是没有进行排序的,所以和例子中选取的1,3,4不同。但仅仅是顺序不同,其实是一样的。
将上面的例子排好序在进行测试
输入
请输入物品个数和背包容量:4 9
请输入每个物品的价值和重量:
4 1
7 2
9 3
10 5
输出
bestp = 23
选取的物品为 :
1 3 4