用回溯算法解决01背包

题目描述

试设计一个用回溯法搜索子集空间树的函数。该函数的参数包括结点可行性判定函数和上界函数等必要的函数,并将此函数用于解 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 + wi <= 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)继续探索其他的可能性。

相关推荐
To_OC9 小时前
LC 994 腐烂的橘子:人人都说是 BFS 入门题,我却写了三遍才过
javascript·算法·leetcode
金銀銅鐵12 小时前
[Python] 扩展欧几里得算法
python·数学·算法
To_OC15 小时前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
To_OC1 天前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
刘马想放假2 天前
Modbus 全栈技术解析:TCP、RTU、ASCII、RTU over TCP
数据结构·网络协议
05Kevin2 天前
lk每日冒险题--数据结构6.27
算法
To_OC2 天前
从一次栈溢出报错说起,我把递归彻底扒明白了
javascript·算法·程序员
千纸鹤安安3 天前
千问Qwen-AgentWorld来了:一个语言模型搞定七大Agent场景,GPT-5.4都输了
算法