题目描述
试设计一个用回溯法搜索子集空间树的函数。该函数的参数包括结点可行性判定函数和上界函数等必要的函数,并将此函数用于解 0-1 背包问题。
0-1 背包问题描述如下:给定 nnn 种物品和一个背包。物品 i 的重量是 wi,其价值为 vi,背包的容量为 C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品 i 只有 2 种选择,即装入背包或不装入背包。不能将物品 iii 装入背包多次,也不能只装入部分的物品 i。
输入
第一行有 2 个正整数 n 和 C,n 是物品数,C 是背包的容量。接下来的 1 行中有 n 个正整数,表示物品的价值。第 3 行中有 n 个正整数,表示物品的重量。
输出
将计算出的装入背包物品的最大价值和最优装入方案输出。第一行输出为:Optimal value is
输入:
5 10
6 3 5 4 6
2 2 6 5 4
输出:
Optimal value is
15
1 1 0 0 1
代码实现:
cs
#include<stdio.h>
#include<stdlib.h>
int n, c; // 物品数量,背包容量
int *w; // 重量数组
int *v; // 价值数组
int *x; // 当前解
int *bestx; // 最优解
int bestv = 0; // 最优价值
int cv = 0; // 当前价值
int cw = 0; // 当前重量
// 上界函数:计算剩余物品的最大可能价值(贪心)
int bound(int i) {
int remain = c - cw; // 剩余容量
int boundv = cv;
// 按单位重量价值降序装入剩余物品
// 先复制剩余物品的索引和单位价值
int *idx = (int*)malloc((n - i + 1) * sizeof(int));
double *unit = (double*)malloc((n - i + 1) * sizeof(double));
for(int k = i; k <= n; k++) {
idx[k - i] = k;
unit[k - i] = (double)v[k] / w[k];
}
// 按单位价值降序排序
for(int p = 0; p < n - i; p++) {
for(int q = p + 1; q < n - i + 1; q++) {
if(unit[p] < unit[q]) {
double temp = unit[p];
unit[p] = unit[q];
unit[q] = temp;
int t = idx[p];
idx[p] = idx[q];
idx[q] = t;
}
}
}
// 贪心装入
for(int k = 0; k < n - i + 1; k++) {
if(remain >= w[idx[k]]) {
boundv += v[idx[k]];
remain -= w[idx[k]];
} else {
boundv += (double)v[idx[k]] / w[idx[k]] * remain;
break;
}
}
free(idx);
free(unit);
return boundv;
}
// 回溯搜索
void backtrack(int i) {
if(i > n) { // 到达叶子节点
if(cv > bestv) {
bestv = cv;
for(int j = 1; j <= n; j++) {
bestx[j] = x[j];//在最后结束的时候将最大价值下的使用情况记录在bestx数组中
}
}
return;
}
if(cw + w[i] <= c){ // 可行性剪枝
cw += w[i];
cv += v[i];
x[i] = 1;
backtrack(i + 1);
cw -= w[i]; // 回溯
cv -= v[i];
x[i] = 0;
}
// 搜索右子树:不选择物品i
if(bound(i + 1) > bestv) { // 上界剪枝
x[i] = 0;
backtrack(i + 1);
}
}
int main() {
scanf("%d%d", &n, &c);
// 分配内存
v = (int*)malloc((n + 1) * sizeof(int));
w = (int*)malloc((n + 1) * sizeof(int));
x = (int*)malloc((n + 1) * sizeof(int));
bestx = (int*)malloc((n + 1) * sizeof(int));
// 读入价值
for(int i = 1; i <= n; i++) {
scanf("%d", &v[i]);
}
// 读入重量
for(int i = 1; i <= n; i++) {
scanf("%d", &w[i]);
}
// 初始化
for(int i = 1; i <= n; i++) {
x[i] = 0;
bestx[i] = 0;
}
// 回溯搜索
backtrack(1);
printf("Optimal value is\n");
// 输出结果(注意题目样例中没有"Optimal value is"这行文字)
printf("%d\n", bestv);
for(int i = 1; i <= n; i++) {
printf("%d", bestx[i]);
if(i < n) printf(" ");
}
printf("\n");
// 释放内存
free(v);
free(w);
free(x);
free(bestx);
return 0;
}
递归树展开:
backtrack(1)
cw=0, cv=0
|
┌─────────────────────┴─────────────────────┐
| |
选物品1 (左子树) 不选物品1(右子树)
cw=2, cv=6 cw=0, cv=0
backtrack(2) backtrack(2)
1.问题困惑
上述题目不是要求只有装入或者不装两种情况吗,为什么还要求单位价值呢?
核心原因:上界剪枝函数的需要
明确:上界剪枝函数是用来估算剩余物品最多还能产生多少价值的。
当在backtrack()函数中出现不满足cw + w[i] <= c这个条件时,也就是说取i这个物品后总重量超过了c,那肯定就不进行if后面的语句,刚好cw不会加上i这个物品,然后我们想要再继续搜索下面的物品,刚好就执行后面的if语句,搜索右子树,即搜索i这个物品后面的其他物品,看看是否满足条件。
那为什么搜索右子树的时候要利用bound函数呢?为什么不能直接backtrack(i+1)呢?
实际上可以,反正都是不满足题意的数要跳过,可以直接用上面的if语句。但这样会失去剪枝效果。
让(i+1)这个物品进行bound()函数操作是为了计算当前节点理论上能达到的最大可能价值(是一个乐观的估计)。然后再与当前已发现的最优解bestv进行比较,如果大于最优解就说明这个分支有可能找到更好的解,所以要继续从当前节点开始往后搜索。如果当前点加上的最大价值比最优解还小,说明这个右子树没必要搜索。
那这个节点左右子树都不符合题意,那就开始进行回溯撤销。
假设i=3时的左右子树都不符合题意,然后开始返回backtrack(2),执行backtrack(i+1),即backtrack(3)后面的语句,就是将2这个节点去掉,然后执行后面的if语句,执行右子树,即选3这个节点。
backtrack(1)
|
├─ 左子树:选1 → backtrack(2) → ... → 返回
|
└─ 右子树:不选1 → backtrack(2)
|
├─ 左子树:选2 → backtrack(3) → 被剪枝,返回
| ↓
| 返回到backtrack(2)
| |
└─ 右子树:不选2 → backtrack(3) → ...
2.剪枝操作:
1.可行性剪枝:如果装不下这个物品,就进行这个物品的剪枝,不搜索选这个分支。避免无效装填
2.上界剪枝:利用上界剪枝函数,如果求得的最大价值小于最优解,就进行右子树的剪枝。避免无希望搜索
因为在01背包中,只有选和不选两种分支。
-
左子树(选):只能用可行性判断来剪枝
-
右子树(不选):只能用上界判断来剪枝
3.我写的代码的问题:
1.不知道当重量之和超过总容量时,应该怎么做。
2.不知道将x数组什么时候输出。主要是因为我忘记当处在子叶位置的时候是结束的时候,应该将最大值判断和x数组赋值。我写的是当达到最大容量的时候就是结束的时候,根据画出来的树状图这肯定是不对的。
搞清楚叶子节点是什么点:叶子节点是物品已经处理完了,每个物品都经历了选与不选。
backtrack(1) → 决定物品1
↓
backtrack(2) → 决定物品2
↓
backtrack(3) → 决定物品3
↓
backtrack(4) → 决定物品4
↓
backtrack(5) → 决定物品5
↓
backtrack(6) → i=6 > n=5,到达叶子节点!
↓
更新最优解
↓
返回 backtrack(5)
到达叶子节点的时候,已经形成了一个完整的符合题意的解。之后就是结束这条路径,返回backtrack(5)继续探索其他的可能性。