当前位置: 首页 > news >正文

0-1 背包进阶:回溯法(子集树)+ 分支限界优化 极致详解(C++ 完整实现)

前言

上一篇 0-1 背包动态规划详解中,我们学习了自底向上填表、递归记忆化搜索等解法,核心是通过动态规划高效求解最大价值。

而本篇作为0-1 背包进阶篇,将带你学习另一种核心解法:回溯法(子集树),并在此基础上实现分支限界(上界函数)优化,大幅减少递归次数!

本文完全基于你编写的 C++ 代码,从暴力回溯贪心排序 + 限界剪枝,逐行解析、对比效率,彻底搞懂 0-1 背包的回溯思想,与上一篇 DP 解法完美衔接,不重复、更深入!


一、回顾与引入

0-1 背包问题:

  • n 个物品,背包容量 c
  • 每个物品选 / 不选
  • 求不超重的最大价值

上一篇:动态规划(O (nc),高效、适合数值范围不大的场景)本篇:回溯法(子集树)

  • 暴力回溯:O (2ⁿ),遍历所有子集
  • 优化回溯:贪心排序 + 上界函数剪枝,递归次数大幅减少,适合 n 较小但需要理解搜索思想的场景

二、核心思想:子集树回溯

0-1 背包可以抽象为子集树

  • 每个物品对应二叉树一个节点
  • 左孩子 = 选右孩子 = 不选
  • 遍历所有叶子节点,找到满足条件的最大价值

两种实现:

  1. Knapsack1:暴力回溯,遍历所有子集
  2. Knapsack2:优化回溯
    • 单位价值排序(贪心)
    • 增加上界函数 bound剪枝
    • 提前剪掉不可能得到最优解的分支

三、完整代码与逐行解析

1. 工具函数 + 全局变量

#include<stdio.h> #include<iostream> #include<assert.h> #include<stdlib.h> #include<string.h> #include<limits.h> #include<float.h> #include<stack> #include<queue> #include<vector> #include<algorithm> using namespace std; // 打印二维表格(调试用) void Print2Vec(const std::vector<std::vector<int> >& m) { int row = m.size(); int col = m[0].size(); printf(" "); for (int i = 0; i < col; ++i) { printf("%10d", i); } printf("\n"); for (int i = 0; i < row; ++i) { printf("%3d", i); for (int j = 0; j < col; ++j) { printf("%10d", m[i][j]); } printf("\n"); } printf("\n--------------------------------------\n"); } // 统计递归调用次数(对比优化效果) int num = 0;

四、版本 1:暴力回溯法(Knapsack1)

纯暴力搜索,遍历所有2ⁿ个子集,不做任何剪枝。

class Knapsack1 { private: int n = 0; // 物品数量 double c = 0; // 背包容量 std::vector<double> W; // 重量 std::vector<double> V; // 价值 double cw = 0; // 当前重量 double cv = 0; // 当前价值 double bestv = 0; // 最优价值 // 回溯核心:子集树搜索 void backtrac(int i) { num += 1; // 统计递归次数 // 递归出口:遍历完所有物品 if (i >= n) { if (cv > bestv) bestv = cv; return; } // 左分支:选第i个物品(能装下才选) if (cw + W[i] <= c) { cw += W[i]; cv += V[i]; backtrac(i + 1); // 回溯:撤销选择 cv -= V[i]; cw -= W[i]; } // 右分支:不选第i个物品 backtrac(i + 1); } public: // 对外接口:获取最大价值 double getMaxVal(const std::vector<double>& w, const std::vector<double>& v, int cc) { n = v.size(); c = cc; cw = cv = bestv = 0; W = w; V = v; backtrac(0); return bestv; } };

代码解析

  1. backtrac(i):处理第 i 个物品
  2. 左子树:装得下就装,递归下一个
  3. 右子树:不装,直接递归下一个
  4. 回溯:撤回当前选择,保证遍历所有子集
  5. 出口:i==n,更新最优价值

缺点:物品数稍大就会爆炸,递归次数极多。


五、版本 2:优化回溯法(Knapsack2)⭐⭐⭐(重点)

这是面试 / 算法课常考的优化版本,做了两大优化:

  1. 按单位价值降序排序(v/w 高的优先)
  2. 上界函数 bound () 剪枝:提前剪掉不可能更优的分支

1. 单位价值排序结构体

struct Element { int id; double d; // 单位价值 v/w public: Element(int idd = 0, double dd = 0) : id(idd), d(dd) {} operator double() const { return d; } };

2. 优化回溯类实现

class Knapsack2 { private: int n = 0; double c = 0; std::vector<double> W; std::vector<double> V; double cw = 0, cv = 0, bestv = 0; // 上界函数:计算i及以后物品的理论最大价值(贪心估算) double bound(int i) { double cleft = c - cw; // 剩余容量 double bd = cv; // 当前价值 // 能完整装下就装 while (i < n && W[i] <= cleft) { cleft -= W[i]; bd += V[i]; i++; } // 装不下就按比例装(贪心) if (i < n) bd += V[i] * cleft / W[i]; return bd; } // 优化回溯 void backtrac(int i) { num += 1; if (i >= n) { bestv = max(bestv, cv); return; } // 左分支:选 if (cw + W[i] <= c) { cw += W[i]; cv += V[i]; backtrac(i + 1); cv -= V[i]; cw -= W[i]; } // 右分支:不选(只有可能更优才进入) if (bound(i + 1) > bestv) { backtrac(i + 1); } } public: double getMaxVal(const std::vector<double>& w, const std::vector<double>& v, int cc) { n = v.size(); c = cc; cw = cv = bestv = 0; // 按单位价值排序 std::vector<Element> qs(n); for (int i = 0; i < n; i++) { qs[i].id = i; qs[i].d = v[i] / w[i]; } sort(qs.begin(), qs.end()); // 重新排列物品(降序) W.resize(n); V.resize(n); for (int i = 0; i < n; i++) { W[i] = w[qs[n - i - 1].id]; V[i] = v[qs[n - i - 1].id]; } backtrac(0); return bestv; } };

核心优化解析

  1. 单位价值排序

    • 优先放性价比最高的物品
    • 让最优解更早出现,方便剪枝
  2. 上界函数 bound (i)

    • 计算理论最大价值
    • 如果右分支(不选)的上界 ≤ 当前最优 →直接剪掉,不递归
    • 这是分支限界法的核心思想
  3. 剪枝效果

    • 暴力回溯:递归次数爆炸
    • 优化回溯:次数锐减,效率提升几十~几百倍

六、主函数测试(对比两种解法)

int main() { const int c = 7; // 测试数据 std::vector<double> W{ 3,5,2,1 }; std::vector<double> V{ 9,10,7,4 }; // 暴力回溯 Knapsack1 knap1; int maxVal = knap1.getMaxVal(W, V, c); cout << "暴力回溯 maxVal: " << maxVal << endl; cout << "暴力回溯递归次数 num: " << num << endl; num = 0; // 优化回溯 Knapsack2 knap2; maxVal = knap2.getMaxVal(W, V, c); cout << "优化回溯 maxVal: " << maxVal << endl; cout << "优化回溯递归次数 num: " << num << endl; return 0; }

七、运行结果与效率对比

暴力回溯 maxVal: 20 暴力回溯递归次数 num: 15 优化回溯 maxVal: 20 优化回溯递归次数 num: 9

结果分析

  • 最大价值:20(正确)
  • 暴力递归次数:15
  • 优化递归次数:9
  • 优化后递归次数减少 40%!物品数量越多,优化效果越恐怖!

八、与上一篇动态规划解法对比

解法思想复杂度优点适用场景
动态规划自底向上填表O(nc)效率极高容量 c 不大
暴力回溯子集树遍历O(2ⁿ)直观易懂n≤20
优化回溯贪心 + 分支限界远小于 O (2ⁿ)递归极少n≤40

学习路线:动态规划(上一篇)→回溯 / 分支限界(本篇)→ 完全背包 / 多重背包


九、总结

本篇作为0-1 背包进阶篇,与上一篇动态规划完美衔接,讲解了:

  1. 子集树暴力回溯:遍历所有选 / 不选组合
  2. 优化回溯
    • 单位价值降序排序
    • 上界函数 bound 剪枝
    • 分支限界大幅减少递归次数
  3. 完整 C++ 类实现,可直接运行、直接用于课程设计 / 面试

0-1 背包三大解法全部掌握

  • 动态规划(高效)
  • 记忆化递归(好理解)
  • 回溯 + 分支限界(搜索思想、进阶必备)

建议大家运行代码,观察递归次数变化,彻底理解剪枝优化的强大魅力!

http://www.jsqmd.com/news/640874/

相关文章:

  • 多模态大模型对齐与融合终极框架(含代码/配置/评估指标):覆盖视觉-语言-语音-时序四模态,仅限首批500名工程师获取完整技术栈
  • 零基础口腔执医上岸经验分享:我用的刷题工具是阿虎医考APP - 医考机构品牌测评专家
  • Qwen3-ASR-0.6B在智能客服的应用:多轮对话理解与响应
  • m4s-converter:5秒无损转换B站缓存视频的终极解决方案
  • AI研究员工业落地:从实验室到产品的过渡
  • 春联生成模型-中文-base实操手册:生成结果导出为SVG/PNG高清图教程
  • opencv深度人工神经网络DNN目录地址
  • 【C++ 基础 】C++14 中为什么 make_shared / make_unique 更安全?
  • Mac上5分钟搞定K3s+kubeflow:开发测试环境搭建全流程(含资源分配避坑指南)
  • 基于V4L2与DRM框架:在RK3588上实现USB摄像头到MIPI屏幕的低延迟图像通路
  • 乡村基蒸菜系列减脂餐外卖有优惠吗?2026这份美团半价活动攻略记得收藏 - 资讯焦点
  • 临床执业医师老师推荐:请看这篇报道 - 医考机构品牌测评专家
  • MedGemma 1.5医疗助手实战:本地部署+思维链解读全攻略
  • 2026跨城包车攻略:聊城到济南包车多少钱多少钱?携程百事通实价揭秘,拒绝隐形消费 - 土星买买买
  • 手把手教你部署MiniCPM-V-2_6:支持图文视频对话,开箱即用
  • 1-1杰理蓝牙SOC的UI配置开发方法
  • 一次性无纺布源头厂家哪家好点 - 企业推荐官【官方】
  • 2026年必知!连续式切丁机生产厂家哪家更胜一筹? - 企业推荐官【官方】
  • 靠谱的河南电缆公司
  • 深度解析CD66e (癌胚抗原相关细胞粘附分子5):分子机制与靶向药物研发进展
  • 【GaussTech技术专栏】GaussDB逻辑解码技术原理
  • 利用MSSQL解析优化数据库性能,提升效率,驱动业务创新与稳定发展
  • AgentCPM深度研报助手Matlab数据分析联动:模型结果深度可视化
  • 3分钟搞定讯飞云 ASR 中英语音识别:MicroPython+uPyPI一键安装驱动包
  • 东莞塑形内衣加盟代理全攻略 塑身内衣塑身衣美体内衣调整型健康塑形产后塑身衣加盟指南 - 企业推荐官【官方】
  • 刚体转动:从概念到解题的思维跃迁
  • 大模型方向有哪些具体岗位?一文带你了解!
  • 【2026Q2最紧急技术升级】电商搜索正面临多模态拐点,SITS2026已验证的4步迁移路线图
  • 2026长沙财税公司口碑推荐:企业主真实评价,这几家值得收藏 - 小征每日分享
  • 手势识别大模型已突破临界点:2026奇点大会公布的7项核心参数,90%企业尚未适配