首页 > 其他分享 >回溯法-0/1背包问题

回溯法-0/1背包问题

时间:2024-09-02 21:53:25浏览次数:13  
标签:背包 问题 遍历 回溯 物品 最优 价值 节点

什么是回溯法?

回溯法是一种搜索算法,它通过深度优先搜索的方式来解决决策问题。它从根节点开始,逐步扩展节点,直到找到所有可能的解。

回溯法的基本思想

  1. 开始节点:从根节点出发,这个节点是解空间的起点。
  2. 扩展节点:在当前节点上,选择一个方向继续搜索,这个方向会形成一个新的节点。
  3. 活节点与死节点:如果新节点有更多选择,它就是活节点;如果所有选择都已尝试,它就是死节点。
  4. 回溯:如果当前路径不是解,回溯到上一个活节点,尝试其他选择。

优化方法

为了提高搜索效率,我们使用两种剪枝技术:

  1. 剪枝一:如果当前物品的加入使得总重量超过背包容量,停止搜索这个方向。
  2. 剪枝二:如果剩余物品即使全部加入,也不能超过当前已知的最优解,停止搜索这个方向。

什么是0/1背包问题?

想象一下,你有一个背包,容量有限。你面前有n种不同的物品,每种物品都有自己的重量和价值。你的目标是选择一些物品放入背包,使得背包里物品的总价值最大,但总重量不能超过背包的容量。

算法描述

给定物品数量n,物品i的重量是wi>0,其价值为vi>0,背包的容量为c。我们的目标是找到一种物品组合,使得总价值最大,且总重量不超过c。

步骤

  1. 初始化:设置一个数组或列表来记录选择的物品。
  2. 递归函数:定义一个递归函数,接收当前物品索引、当前重量和当前价值作为参数。
  3. 边界条件:如果当前重量超过背包容量或已处理完所有物品,回溯。
  4. 选择与不选择:对于每种物品,尝试选择它或不选择它,然后递归调用函数。
  5. 更新最优解:每次找到一个解时,比较并更新已知的最优解。

0/1背包问题算法设计

算法目标

我们的目标是找出一个解向量 ( xi ),其中 ( xi = 0 ) 表示不放入物品 ( i ),( xi = 1 ) 表示放入物品 ( i )。

递归函数 Backtrack

  1. 叶子节点:如果 ( i > n ),我们到达了一个新的物品装包方案,更新最优价值。
  2. 扩展节点:如果 ( i < n ),当前节点在排列树的第 (i-1) 层,递归搜索子树,剪去不满足约束的节点。

实例

输入

  • 物品价值 ( V = {12, 11, 9, 8} )
  • 物品重量 ( W = {8, 6, 4, 3} )
  • 背包容量 ( B = 13 )

可行解

  • 解1: ( x = <0, 1, 1, 1> ) 选入物品2, 3, 4,总价值28,总重量13
  • 解2: ( x = <1, 0, 1, 0> ) 选入物品1, 3,总价值21,总重量12

最优解

  • 最优解: ( x = <0, 1, 1, 1> )

定义

  • ( CW )(Current Weight): 当前重量
  • ( CP )(Current Price): 当前价值

执行步骤

  1. 计算单位价值:降序排列物品。
  2. 从根节点出发:根节点代表当前扩展节点。
  3. 搜索左子树:判断物品是否装入背包。
    • 可行,更新 ( CW ) 和 ( CP ),继续遍历。
    • 不可行,回溯,尝试右子树。
  4. 计算上界 ( bound(i) ):
    • 若 ( bound(i) < bestp ),剪枝。
    • 否则,继续搜索。
  5. 叶子节点:比较 ( CP ) 与 ( bestp ),更新 ( bestp )。
  6. 遍历所有节点:完成搜索。

举例说明

已知 ( p = {45, 25, 24} ), ( w = {16, 15, 15} ), 背包容量为30,求最优价值。

步骤1:计算单位价值并排序

首先,我们需要计算每个物品的单位价值,即每个物品的价值除以其重量。然后,我们将物品按照单位价值从高到低进行排序。在这个例子中,物品的原始顺序恰好是单位价值降序排列。

步骤2:开始遍历并判断是否装入背包

我们将按照排序后的物品顺序,逐个考虑每个物品是否装入背包。

遍历过程说明

  • B1, B2:代表同一种物品B的不同节点。
  • C1, C2, C3, C4:代表同一种物品C的不同节点。
  • 这样的表示方法有助于我们区分在遍历过程中的不同节点。

具体步骤

  1. 考虑物品B1:首先尝试将物品B1装入背包。如果B1的重量加上当前背包重量(CW)不超过背包总容量(本例中为30),则B1可以装入背包。此时,背包重量更新为CW = 16,背包价值更新为CP = 45。

  2. 遍历B1的左子树:继续考虑B1后面的物品,如果剩余容量不足以装入下一个物品,我们就剪去这条路径。

  3. 进入B1的右子树:如果B1可以装入,我们继续考虑其他物品。在右子树中,我们到达物品C2,并计算上界值bound(i)。如果bound(i)大于当前最优价值bestp,则继续向下遍历。

  4. 到达叶子节点:如果在遍历中到达叶子节点,我们比较当前价值(CP)与最优价值(bestp),如果CP更大,则更新最优价值。

  5. 回溯:如果发现某条路径不可能产生更好的解,我们回溯到上一个决策点,尝试其他可能性。

示例遍历

  • 装入物品B1,CW = 16, CP = 45。
  • 尝试装入物品C1,但因剩余容量不足而剪枝。
  • 继续考虑C2,计算bound(i)=45+(25/15)**14=45+1.66*14=68.3,大于当前最优价值45,继续遍历。
  • 到达C2的叶子节点,记录最优价值bestp = 45。
  • 回溯,尝试其他物品B2,更新bound(i)为49,继续遍历。
  • 装入物品C3,CW = 15, CP = 25,继续考虑下一个物品。
  • 装入物品D5,CW = 30, CP = 49,更新最优价值bestp = 49。
  • 继续回溯,考虑其他可能的组合直到所有节点遍历完毕。

结果

经过所有可能的遍历和回溯,我们发现最优的背包装载方案价值为49,对应的物品组合为CD。

遍历过程图示

在这里插入图片描述
bound = 45+14*(24/15)=67.4

代码

#include <iostream> // 引入标准输入输出流库
#include <stdio.h>  // 引入C标准库,提供输入输出函数
using namespace std; // 使用标准命名空间

// 定义全局变量
int n; // 物品数量
double c; // 背包容量
double v[100]; // 各个物品的价值数组
double w[100]; // 各个物品的重量数组
double cw = 0.0; // 当前背包重量
double cp = 0.0; // 当前背包中物品的总价值
double bestp = 0.0; // 记录找到的最优价值
double perp[100]; // 存储物品的单位价值,用于排序
int order[100]; // 存储物品的原始索引,用于排序后恢复
int put[100]; // 标记每个物品是否被选中放入背包,1表示放入,0表示不放入

// 按单位价值对物品进行排序的函数
void knapsack() {
    int i, j; // 循环变量
    int temporder = 0; // 用于交换的临时变量
    double temp = 0.0; // 用于交换的临时变量

    // 计算每个物品的单位价值并存放到数组perp中
    for(i = 1; i <= n; i++) {
        perp[i] = v[i] / w[i];
    }

    // 使用冒泡排序算法按单位价值对物品进行排序
    for(i = 1; i <= n - 1; i++) {
        for(j = i + 1; j <= n; j++) {
            // 如果当前物品的单位价值小于下一个物品,则交换它们的位置
            if(perp[i] < perp[j]) {
                // 交换perp数组中的元素
                temp = perp[i];
                perp[i] = perp[j];
                perp[j] = temp;

                // 交换order数组中的元素,以保持物品原来的顺序
                temporder = order[i];
                order[i] = order[j];
                order[j] = temporder;

                // 交换v数组中的元素,以保持物品价值的一致性
                temp = v[i];
                v[i] = v[j];
                v[j] = temp;

                // 交换w数组中的元素,以保持物品重量的一致性
                temp = w[i];
                w[i] = w[j];
                w[j] = temp;
            }
        }
    }
}

// 回溯函数,用于搜索最优解
void backtrack(int i) {
    // i表示当前正在考虑的物品索引
    if(i > n) { // 如果已经考虑完所有物品,则结束递归
        bestp = cp; // 更新最优价值为当前价值
        return;
    }
    // 如果当前物品可以放入背包,更新背包状态并继续搜索左子树
    if(cw + w[i] <= c) {
        cw += w[i]; // 将物品重量加到当前背包重量
        cp += v[i]; // 将物品价值加到当前背包价值
        put[i] = 1; // 标记当前物品已放入背包
        backtrack(i + 1); // 递归搜索下一件物品
        // 回溯,撤销上一步操作
        cw -= w[i];
        cp -= v[i];
        put[i] = 0;
    }
    // 计算当前扩展节点的上界,如果上界大于当前最优价值,则继续搜索右子树
    double boundValue = bound(i + 1);
    if(boundValue > bestp) {
        backtrack(i + 1);
    }
}

// 计算上界函数,用于剪枝以减少搜索空间
double bound(int i) {
    // 计算剩余背包容量
    double leftw = c - cw;
    double b = cp; // 当前背包的总价值
    // 遍历剩余物品,尝试以单位价值递减的顺序装入背包
    while(i <= n && w[i] <= leftw) {
        leftw -= w[i]; // 更新剩余容量
        b += v[i]; // 更新总价值
        i++; // 移动到下一个物品
    }
    // 如果还有剩余容量,尝试用最大单位价值的物品填充
    if(i <= n) {
        b += (v[i] / w[i]) * leftw;
    }
    return b; // 返回计算出的上界
}

// 主函数,程序入口点
int main() {
    int i; // 循环变量
    // 从用户那里获取物品数量和背包容量
    printf("请输入物品的数量和背包的容量:");
    scanf("%d %lf", &n, &c);

    // 从用户那里获取每个物品的重量
    printf("请依次输入%d个物品的重量:\n", n);
    for(i = 1; i <= n; i++) {
        scanf("%lf", &w[i]);
        order[i] = i; // 初始化物品的原始索引
    }

    // 从用户那里获取每个物品的价值
    printf("请依次输入%d个物品的价值:\n", n);
    for(i = 1; i <= n; i++) {
        scanf("%lf", &v[i]);
    }

    // 调用排序函数和回溯函数
    knapsack();
    backtrack(1);

    // 输出最优价值和需要装入背包的物品编号
    printf("最优价值为:%lf\n", bestp);
    printf("需要装入的物品编号是:");
    for(i = 1; i <= n; i++) {
        if(put[i] == 1) {
            printf("%d ", order[i]);
        }
    }
    printf("\n"); // 输出换行符,美化输出格式
    return 0; // 程序正常结束
}

在这里插入图片描述

时间复杂度

因为物品只有选与不选2个决策,而总共有n个物品,所以时间复杂度为在这里插入图片描述

因为递归栈最多达到n层,而且存储所有物品的信息也只需要常数个一维数组,所以最终的空间复杂度为O(n)。

标签:背包,问题,遍历,回溯,物品,最优,价值,节点
From: https://blog.csdn.net/qq_52291558/article/details/141716725

相关文章

  • 动态规划法-资源分配问题
    动态规划法-资源分配问题问题描述把4个份额的资源分配给3个工程,给定利润表如下表所示,写出资源的最优分配方案的求解过程。4份资源分配给3个工程的利润表步骤一:求各个阶段不同分配份额时的最大利润及分配份额目标我们的目标是找到在给定资源限制下,如何分配资源给不......
  • Java 面试题:事务隔离级别以及并行事务会出现什么问题&&怎么解决脏读、不可重复读和幻
    文章目录四种事务隔离级别MySQL中设置事务隔离级别四种事务隔离级别在并行事务中可能会遇到的问题脏读、不可重复读和幻读三者区别事务的隔离级别是怎么解决这三个问题的?ReadView是什么ReadView包含的信息ReadView在MVCC中的工作原理工作流程总结事务的隔......
  • Bean的循环依赖问题
    什么是Bean的循环依赖Bean的循环依赖,就是A对象中有B属性,B对象中有A属性。即我依赖你,你也依赖我。也就是两个或多个对象之间相互引用成环。比如:丈夫类Husband,妻子类Wife,Husband中有Wife的引用,Wife中有Husband的引用。packagecw.spring.bean;/***ClassName:Wife*P......
  • 彻底解决Flutter项目底部导航栏穿透问题
    项目背景:在学习比站猫哥的“2022Flutter3GetxWoocommerceApp从零开始实战课程|01课程”时,按照课程指导逐步进行项目代码编写。视频地址:https://www.bilibili.com/video/BV1xY411F7es/?spm_id_from=333.999.0.0&vd_source=7c7ae5cc1dbb2453e1eb43950a4264a3。(1)问题表现:底......
  • 如何训练一个 LSTM 网络以解决特定的序列预测问题(含代码示例)
    关注我,持续分享逻辑思维&管理思维&面试题;可提供大厂面试辅导、及定制化求职/在职/管理/架构辅导;推荐专栏《10天学会使用asp.net编程AI大模型》,目前已完成所有内容。一顿烧烤不到的费用,让人能紧跟时代的浪潮。从普通网站,到公众号、小程序,再到AI大模型网站。干货满满。学成后可......
  • git-repo https证书认证问题
    目录问题描述解决步骤1.下载证书2.测试证书是否正常3.设置环境变量总结问题描述在使用gitrepo同步仓库时,发现不能同步,出现如下提示错误:%Total%Received%XferdAverageSpeedTimeTimeTimeCurrentDloadUploadTotalSpentLeftS......
  • vue 遇到的问题 以后看一下
    <template>  <div>   <el-button@click="openMessageBox">打开MessageBox</el-button>   <el-button@click="updateMessageBoxContent">打开MessageBox</el-button>   </div> </template>......
  • 解决平方矩阵问题
    问题输入整数N,输出一个N阶的二维数组。数组的形式参照样例。输入格式输入包含多行,每行包含一个整数N。当输入行为N=0时,表示输入结束,且该行无需作任何处理。输出格式对于每个输入整数N,输出一个满足要求的N阶二维数组。每个数组占N行,每行包含N个用空格隔开的整数。每个......
  • 解决UE的Chunk(数据块)功能疑似无法生效的问题
    先说结论,ue的Chunk功能能够正常使用,通过官网的教程配置完毕后就能正常使用了,虚幻引擎中的内容烘焙和数据块划分|虚幻引擎5.4文档|EpicDeveloperCommunity(epicgames.com)。我们有时遇到无法使用的原因是存在中文路径,在打包项目期间,可以通过在下面的目录中看到每个数据库......
  • Capital许可管理常见问题解答
    在软件资产管理过程中,企业经常会遇到各种关于许可管理的问题。这些问题不仅影响软件的合规使用,还可能导致不必要的法律风险和成本浪费。作为专业的软件许可管理解决方案提供商,Capital致力于帮助企业轻松应对这些挑战。以下是Capital许可管理中常见的问题及其解答,助您更好地理解和......