news 2026/4/30 16:53:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 背包三大解法全部掌握

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 21:59:09

隧道灯售后完善生产厂家筛选要点(工程实用版)

从工程技术实操视角来看&#xff0c;隧道灯售后完善生产厂家&#xff0c;不仅要售后响应及时&#xff0c;还要能提供合规的产品检验报告和运维支持一、售后完善的厂家&#xff0c;产品性能必达国标&#xff08;不踩坑基础&#xff09;我发现很多小白有个误区&#xff0c;觉得售…

作者头像 李华
网站建设 2026/4/14 18:55:27

网络安全检查表 docx 附文件

「【模板】2.15-1网络安全检查表.docx」 /~7cac3Y7Y4m~:/ 链接&#xff1a;https://pan.quark.cn/s/b08da85c00e41已根据您提供的内容&#xff0c;整理出完整的《网络安全检查评估表》。检查指标检查要素检查标准检查结果网络安全 组织管理网络安全主管领导明确一名主管领导负责…

作者头像 李华
网站建设 2026/4/14 18:52:45

零宽度字符的隐秘力量:从文本处理到数据加密的实战指南

1. 零宽度字符&#xff1a;看不见的文本魔术师 第一次听说零宽度字符时&#xff0c;我正被一个诡异的Bug困扰——两个看似相同的字符串在程序里死活匹配不上。直到把文本粘贴到代码编辑器&#xff0c;才发现在肉眼不可见的地方藏着几个"幽灵字符"。这就是零宽度字符给…

作者头像 李华
网站建设 2026/4/14 18:51:41

Linux03-主机规划与磁盘划分

一、认识服务器的硬件 1.1 硬件结构1.项盖6.前置硬盘11.风扇16.PCIe卡2.散热器7.DVD光驱12.导风罩17.I/O模组3.M.2接口固态硬盘8.RAID控制器卡13.后置硬盘背板18.I/O模组4.内存条9.前置硬盘背板14.硬盘19.板载网卡5.主板托架10.风扇框15.电源模块说明: 内存边的两个黑色小框是C…

作者头像 李华