用回溯算法解决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 + 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)继续探索其他的可能性。

相关推荐
Shan12051 小时前
二叉树的遍历算法之中序遍历
算法
Odedipus1 小时前
二叉树的学习笔记
数据结构·笔记·学习
晨曦中的暮雨1 小时前
动态规划专题Day1——打家劫舍系列
算法·动态规划
khalil10201 小时前
代码随想录算法训练营Day-52 图论03 | 101.孤岛的总面积、102.沉没孤岛、103.水流问题、104.建造最大岛屿
c++·算法·图论
老四啊laosi1 小时前
[滑动窗口] 13. 水果成篮
算法·leetcode·滑动窗口·水果成篮
刀法如飞1 小时前
Palantir技术原理深度分析:Ontology 存储结构与读写方式
人工智能·算法·架构
澈2071 小时前
图论基础:邻接矩阵与邻接表详解
算法·图论·邻接矩阵
白日做梦Q2 小时前
Miniconda 新手保姆级教程:从安装到熟练使用(全程无跳步,避坑指南附全)
人工智能·深度学习·算法·机器学习
吃好睡好便好2 小时前
在Matlab中绘制变半径柱面图
开发语言·人工智能·学习·算法·matlab