0-1背包问题是子集选取问题。一般情况下,0-1背包问题是NP难的,0-1背包问题的解空间可用子集树表示。解0-1背包问题的回溯法与解装载问题的回溯法十分相似。
在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。设r是当前剩余物品价值总和:cp是当前价值;bestp是当前最优价值。当cp+r<=bestp时,可剪去右子树。计算中解的上界的更好方法是剩余物品依其单位重量价值排序,然后依次装入物品,直到装不下时,再装入该物品的一部分而装满背包。由此得到的价值是右子树解的上界。
例如,对于0-1背包问题的一个实例,n=4,c=7,p=[9,10,7,4],w=[3,5,2,1].这个物品的单位重量价值分别为[3,2,3,5,4],以物品单位重量价值的递减序装入物品,先装入物品4,然后装入物品3和1,。装入这3个物品后,剩余的背包容量为1,只能装入0.2的物品2.由此得到一个解为x = [1,0.2,1,1],其相应的价值为22.尽管这不是一个可行解,但可以证明其价值是最优解的上界。因此,对于这个实例,最优值不超过22。
输入:物品的数目n,背包的容量c。各个物品的重量wi,各个物品的价值vi。
输出:装入背包的最大价值。
运行结果:
为了便于计算上界,可先将物品依其单位重量价值从大到小排序,此后只要按顺序考察各物品即可。在实现时,由Bound计算当前结点处的上界。类Knap的数据成员记录解空间树中的结点信息,以减少参数传递及递归调用所需的栈空间。在解空间树的当前扩展结点处。仅当要进入右子树时才计算上界Bound,以判断是否可将右子树剪去。进入左子树时不需计算上界,因为其上界与其父结点的上界相同。
template
class Knap
{
template
friend Tp Knapsack(Tw *, Tp *, Tw, int);
private:
Typep Bound(int i);
void BackTrace(int i);
int n; //装包物品重量
Typew c, //背包容量
*w, //物品重量数组
cw; //当前重量,从根到当前结点所形成的部分解的装包物品重量
Typep cp, //当前价值,从根到当前结点所形成的部分解的装包物品价值
*p, //物品价值数组
bestp; //当前最优价值,已搜索过得部分解空间树的最优装包价值
};
//计算以当前结点为根的子树的价值上界
//计算上界的方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包。
template
Typep Knap::Bound(int i)
{
Typew cleft; //剩余容量
Typep b;
cleft = c - cw;
b = cp;
while(i <= n && w[i] <= cleft) //以物品单位重量价值递减序装入物品
{
b += p[i];
cleft -= w[i];
i++;
}
if(i <= n) //装满背包,剩余的容量不足一个,装一部分
b += p[i]*cleft / w[i];
return b;
}
//对解空间树回溯搜索,求得最大装包价值
template
void Knap::BackTrace(int i)
{
if(i > n) //到达叶节点
{
bestp = cp;
return;
}
if(cw + w[i] <= c) //满足约束函数,进入左子树
{
cw += w[i];
cp += p[i];
BackTrace(i+1);
cw -= w[i]; //回溯还原
cp -= p[i];
}
if(Bound(i+1) > bestp) //满足限界函数,进入右子树
BackTrace(i+1);
}
//物品
class Object
{
template
friend Tp Knapsack(Tw *, Tp *, Tw, int);
public:
bool operator < (const Object &a) const
{
return d > a.d;
}
private:
int ID; //物品标号
float d; //单位重量价值
};
template
Typep Knapsack(Typew *w, Typep *p, Typew c, int n)
{
Typew W;
Typep P;
Object *Q;
int i;
//初始化
W = 0;
P = 0;
Q = new Object[n];
for(i = 1; i <= n; i++)
{
Q[i-1].ID = i;
Q[i-1].d = 1.0*p[i] / w[i];
W += w[i];
P += p[i];
}
if(W <= c) //能够装入所有物品
return P;
sort(Q, Q+n); //将n个物品依单位重量价值排序
Knap K;
K.c = c;
K.n = n;
K.bestp = 0;
K.cw = 0;
K.cp = 0;
K.p = new Typep[n+1];
K.w = new Typew[n+1];
for(i = 1; i <= n; i++)
{
K.w[i] = w[Q[i-1].ID];
K.p[i] = p[Q[i-1].ID];
}
K.BackTrace(1);
delete []Q;
delete []K.p;
delete []K.w;
return K.bestp; //返回最大装包价值
}
2、算法效率
计算上界需要O(n)时间,在最坏情况下有个右儿子结点需要计算上界,故解0-1背包问题的回溯算法Backtrack所需的计算时间为.