描述:n种物品和一个背包,物品i的重量是wi,价值是vi,背包容量是c。求解一种放入背包中物品的选择,使得背包中物品总价值最大。
在选择装入背包的物品时,对物品i只有两种选择,要么完全装入,要么不装入背包,不能装入多次,不能只装入部分。
此问题可以数学描述为,求解一个n元0-1向量{x0,x1,...,xn-1};其中x0为1时表示装入物品0,为0时表示不装入物品0,其余属性类推。
这个向量满足两个方面:
1:sum(xi*wi) <= c
2:sum (xi*vi)在满足条件1的情况下最大。
回溯解法:最容易想到的是回溯法,遍历整个解空间,找到合适的解,而这个问题的解空间是一个子集树,子集树就是组合树,即集合中可以选取的元素组合。对于每个位置的元素都有选取和不选取两种选择,定义一个与元素集合等长的数组j,令选取某个位置pos的元素时其在数组j中对应位置的元素j[pos]=1,否则j[pos]=0,于是得到j数组所有的取值情况如下图。
遍历这个解空间有一个框架代码如下,从数组的第一位开始pos = 0,直到pos大于等于数组长度结束,将arr[pos]置于1递归一层,然后再将arr[pos]置于0时递归一层。这样递归一层,规模扩大为原来的两倍,所以这个过程的复杂度为2的n次方,n为递归的层数。
void func (int *arr, int len, int pos) {
if (pos >= len) {
printArr (arr, len);
return;
}
arr[pos] = 0;
func (arr, len, pos+1);
arr[pos] = 1;
func (arr, len, pos+1);
}
每次pos>=len时说明生了了一种子集和方案,最容易想到的方法是根据计算这个生成的子集和方案,计算其选取的子集和的总重量及总价值,只要是总重量小于c,而总价值大于当前保存的最大总价值,就替换当前保存的集合。设计根据子集和计算总价值或总重量的函数如下:
int caculatev (int *a, int *p, int n) {
int i, sum = 0;
for (i = 0; i < n; ++i) {
sum += p[i] ? a[i] : 0;
}
return sum;
}
其中p数组中每个元素的取值是0或1,为0时表示没有选择数组a对应位置上的元素,为1时说明选择了数组a对应位置的元素,有了上面的函数,我们带入框架代码如下:
void knapback (int *w, int *v, int n, int c, int *m, int pos, int *b,int *r) {
if (pos >= n) {
int sumv = caculatev (v, b, n); // 计算当前子集和的总价值
int sumw = caculatev (w, b, n); // 计算当前子集和的总重量
if (sumw <= c && sumv > *m) { // 判断
*m = sumv;
memcpy (r, b, sizeof(int)*n);
}
return;
}
b[pos] = 1;
knapback (w,v,n,c,m,pos+1,b,r);
b[pos] = 0;
knapback (w,v,n,c,m,pos+1,b,r);
}
很明显,上面的方案是很低效的,因为只是遍历了解空间,而且对解空间中的每个解都计算了其总价值和总重量,没有任何剪枝的动作,因此我们可以讲计算总价值和总重量的过程加入到迭代解空间的过程中,如果在迭代某个解的非最底层发现其总价值已经不可能大于当前保存的最优解情况,或者背包已经没有容量就剪掉该解。设计代码如下:
void knapback (int *w, int *v, int n, int c, int cur, int rv, int *m,
int pos, int *b,int *r) {
if (pos >= n) {
if (cur > *m) {
*m = cur;
memcpy (r, b, sizeof(int)*n);
}
return;
}
if (cur+rv < *m) {
return;
}
if (c >= w[pos]) {
c -= w[pos];
b[pos] = 1;
knapback (w,v,n,c,cur+v[pos],rv-v[pos],m,pos+1,b,r);
c += w[pos];
b[pos] = 0;
}
knapback (w, v, n, c, cur, rv-v[pos],m, pos+1, b, r);
}
其中:
w是重量数组;v是价值数组;n是长度数组;c是背包能容纳的总容量;pos是当前函数遍历解空间时处于解空间的层数;
cur函数当前所处解空间分支到达pos层时的价值和;rv一开始是v数组的和,没迭代一层就减去v数组中这一轮的值
m是遍历解空间过程中保留的当前最价值;b数组用作遍历解空间用,等同于上面框架代码的arr;r数组用于保存遍历解空间过程中的m的解。