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

动态规划01背包

0/1 背包是什么?

  • 你有 n 个物品,第 i 个物品有重量 w[i]价值 v[i]

  • 有一只容量为 W 的背包,只能装重量和 ≤ W 的东西。

  • 0/1 的意思:每个物品要么不拿,要么拿一次(不能拆,也不能重复拿)。

  • 目标:在不超过容量的前提下,让总价值最大


一、二维 DP(最直观、最好理解)

1. 状态定义

dp[i][c]:只考虑前 i 个物品,在背包容量为 c 时能得到的最大价值

行是“用了多少件物品”,列是“当前容量”。
这就像一张表,我们从小问题(少物品、小容量)一步步填到大问题(多物品、大容量)。

2. 转移方程

对第 i 件物品(下标从 1 开始、重量 w[i],价值 v[i]),对每个容量 c

  • 不选第 i 件:dp[i][c] = dp[i-1][c]

  • 可选且选择:如果 c >= w[i]
    dp[i][c] = max(dp[i-1][c], dp[i-1][c - w[i]] + v[i])

逻辑:选了它,就占用 w[i] 容量并获得 v[i] 价值,剩下的容量在“前 i-1 件物品”里去凑。

3. 初始条件

  • dp[0][c] = 0(没有物品时,价值为 0)

  • dp[i][0] = 0(容量为 0 时,背不了东西)

4. 代码(带极详细注释)

#include <bits/stdc++.h>
using namespace std;// 二维 DP:dp[i][c] = 前 i 件物品、容量 c 的最大价值
int knapsack_2d(const vector<int>& w, const vector<int>& v, int W) {int n = (int)w.size();vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));// 物品按 1..n 编号使用(w[i-1]/v[i-1] 访问原数组)for (int i = 1; i <= n; ++i) {for (int c = 0; c <= W; ++c) {// 不选第 i 件dp[i][c] = dp[i - 1][c];// 能选就尝试选一下,看是否更优if (c >= w[i - 1]) {dp[i][c] = max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]);}}}return dp[n][W];
}

5. 选哪些物品?(回溯路径)

二维 DP 很容易“把方案捞出来”:从 dp[n][W] 倒着看:

  • 如果 dp[i][c] == dp[i-1][c] → 第 i没选

  • 否则必有 dp[i][c] == dp[i-1][c - w[i-1]] + v[i-1] → 第 i选了c -= w[i-1],继续看 i-1

vector<int> reconstruct_items(const vector<int>& w, const vector<int>& v, int W) {int n = (int)w.size();vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));for (int i = 1; i <= n; ++i) {for (int c = 0; c <= W; ++c) {dp[i][c] = dp[i - 1][c];if (c >= w[i - 1]) {dp[i][c] = max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]);}}}// 回溯选品vector<int> picked; // 存下标(0..n-1)int c = W;for (int i = n; i >= 1; --i) {// 若选择更优,说明选了第 i 件if (c >= w[i - 1] && dp[i][c] == dp[i - 1][c - w[i - 1]] + v[i - 1]) {picked.push_back(i - 1);c -= w[i - 1];}// 否则没选,啥也不做}reverse(picked.begin(), picked.end());return picked;
}

二、一维 DP(空间优化,面试最常写)

二维表每次只用到上一行,所以可以压缩到一维数组:dp[c] 表示当前处理到的物品范围内,容量为 c 的最大价值。

关键点:容量 c 要倒序遍历!

  • 公式:dp[c] = max(dp[c], dp[c - w[i]] + v[i])(当 c >= w[i]

  • 为什么倒序? 如果正序 c=0..W,你刚更新完 dp[c],后面更新 dp[c + w[i]] 时会用到“本轮已更新过”的 dp[c],等价于重复使用了第 i 件物品(就变成了完全背包),所以 0/1 背包必须倒序。

int knapsack_1d(const vector<int>& w, const vector<int>& v, int W) {int n = (int)w.size();vector<int> dp(W + 1, 0);for (int i = 0; i < n; ++i) {for (int c = W; c >= w[i]; --c) { // 倒序,防止重复使用同一物品dp[c] = max(dp[c], dp[c - w[i]] + v[i]);}}return dp[W];
}

三、手工推一遍(加深理解)

例子:W = 5
物品(w, v):

  1. (2, 3)

  2. (3, 4)

  3. (4, 8)

  4. (5, 8)

最优答案是 8(拿第 3 件 (4,8) 或第 4 件 (5,8))。
看一维 DP 如何“长出来”:

  • 初始:dp = [0,0,0,0,0,0](索引是容量 0..5)

  • 物品1 (2,3):倒序更新 c=5..2

    • c=5: max(0, dp[3]+3=0+3)=3[0,0,0,0,0,3]

    • c=4: max(0, dp[2]+3=0+3)=3[0,0,0,0,3,3]

    • c=3: max(0, dp[1]+3=0+3)=3[0,0,0,3,3,3]

    • c=2: max(0, dp[0]+3=0+3)=3[0,0,3,3,3,3]

  • 物品2 (3,4):c=5..3

    • c=5: max(3, dp[2]+4=3+4=7)=7[0,0,3,3,3,7]

    • c=4: max(3, dp[1]+4=0+4=4)=4[0,0,3,3,4,7]

    • c=3: max(3, dp[0]+4=0+4=4)=4[0,0,3,4,4,7]

  • 物品3 (4,8):c=5..4

    • c=5: max(7, dp[1]+8=0+8=8)=8[0,0,3,4,4,8]

    • c=4: max(4, dp[0]+8=0+8=8)=8[0,0,3,4,8,8]

  • 物品4 (5,8):c=5..5

    • c=5: max(8, dp[0]+8=8)=8[0,0,3,4,8,8]

最终 dp[5]=8,和我们直觉一致。


四、常见易错点(务必注意)

  1. 一维写法容量必须倒序;正序会变“完全背包”(可以无限取同一件)。

  2. 别忘了容量从 w[i] 开始c >= w[i] 时才可转移)。

  3. 初始化:dp 默认 0 就行(0/1 背包是“最大化”,且允许不拿)。

  4. 权重/价值可能比较大时,用 long long(尤其是多维扩展时)。

  5. 如果要恢复选品,建议用二维 DP(或维护 keep[i][c] 布尔表)。

  6. 若题目要求“恰好装满”而不是“容量不超过”,初始化会不一样(需要用 -INF/不可达表示);标准 0/1 背包默认“≤W”即可。


五、完整小模板(便于粘贴)

#include <bits/stdc++.h>
using namespace std;// 一维 0/1 背包:空间 O(W),时间 O(nW)
int knap01_1d(const vector<int>& w, const vector<int>& v, int W) {int n = (int)w.size();vector<int> dp(W + 1, 0);for (int i = 0; i < n; ++i) {for (int c = W; c >= w[i]; --c) { // 倒序!dp[c] = max(dp[c], dp[c - w[i]] + v[i]);}}return dp[W];
}// 二维 0/1 背包:便于恢复选品
pair<int, vector<int>> knap01_2d_with_pick(const vector<int>& w, const vector<int>& v, int W) {int n = (int)w.size();vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));for (int i = 1; i <= n; ++i) {for (int c = 0; c <= W; ++c) {dp[i][c] = dp[i - 1][c];if (c >= w[i - 1]) {dp[i][c] = max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]);}}}// 回溯选品vector<int> pick;int c = W;for (int i = n; i >= 1; --i) {if (c >= w[i - 1] && dp[i][c] == dp[i - 1][c - w[i - 1]] + v[i - 1]) {pick.push_back(i - 1);c -= w[i - 1];}}reverse(pick.begin(), pick.end());return {dp[n][W], pick};
}

六、拓展一口:与其它背包的区别

  • 完全背包(可无限拿):一维写法容量正序for c=w..W),因为同一轮允许复用当前物品。

  • 多重背包(每件有数量上限):常用二进制拆分成若干 0/1 物品,或用单调队列优化。

  • 恰好装满:初始化不同(不可达状态要用 -INF),目标是 dp[W] 可达且最大。

疑问:

// 计算“前 i 件物品、容量为 c”的最优值
dp[i][c] = dp[i-1][c];  // 不选第 i 件
if (c >= w[i-1]) {      // ✅ 只有当容量 c 足以容纳第 i 件时dp[i][c] = max(dp[i][c],dp[i-1][c - w[i-1]] + v[i-1]); // 选第 i 件
}

你问“为什么写 c >= w[i-1],而不是‘减去 i 的重量’”。其实**“减去 i 的重量”就在右边的索引里**:dp[i-1][c - w[i-1]]
我们是在计算状态 dp[i][c] 时,考虑“选第 i 件”的方案,此时剩余容量就是 c - w[i-1],所以要从上一行(只用前 i-1 件)取到 dp[i-1][c - w[i-1]] 再加上这件的价值 v[i-1]。这里并不是要去修改循环变量 c,而是用“剩余容量”的状态值来转移

if (c >= w[i-1]) 的作用有两点:

  1. 可行性检查:只有当当前容量 c 足够装下第 i 件(c - w[i-1] >= 0)时,才允许“选它”的分支;否则这件装不下,不能用这个转移。

  2. 防止越界:避免访问 dp[i-1][负数] 这种非法下标。

为什么是“”而不是 “>”?

  • c == w[i-1] 时,正好把这件装满也是合法的,必须允许,所以用 >=

  • 如果用 >,就会错误地禁止“正好装满”的情况。

再补两点对比,避免常见误解:

  • 不要改动循环变量 c(比如写成 c -= w[i-1]):
    我们是在“同一个 c”下比较两种选择(选/不选),改了 c 就把状态弄混了,而且会影响后续循环。

  • 一维优化时看起来没有 if
    常见的一维 0/1 背包写法是

    for (int i = 0; i < n; ++i)for (int c = W; c >= w[i]; --c)   // 从 W 到 w[i] 逆序dp[c] = max(dp[c], dp[c - w[i]] + v[i]);
    

    这里把“c >= w[i]”体现在循环边界里了(从 w[i] 开始倒着遍历),本质和二维版的 if (c >= w[i-1]) 完全一样。

一个小例子

容量 c=5,第 i 件 w=3, v=4

  • 计算 dp[i][5]:可以不选 → dp[i-1][5];也可以选 → dp[i-1][5-3] + 4 = dp[i-1][2] + 4。两者取大。

  • 计算 dp[i][2]:因为 2 < 3,选不了这件,只能 dp[i][2] = dp[i-1][2]
    这正是 if (c >= w) 的意义。

一句话总结:
c >= w[i-1] 是在判断“当前容量是否足以装下这件”,而真正的“减去 i 的重量”体现在转移来源 dp[i-1][c - w[i-1]] 上;我们不改变 c,而是用“剩余容量”的最优值来更新当前状态。

习题:

46. 携带研究材料(第六期模拟笔试)

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){int m ,n;cin >> m >> n;vector<int> M(m,0); // 重量vector<int> N(m,0); // 价值for(auto & x: M){cin >> x;}for(auto & x : N){cin >> x;}vector<int> dp(n+1,0);for(int i = 0; i < m; i++){for(int j = n; j >= M[i]; j--){dp[j] = max(dp[j], dp[j-M[i]] + N[i]);}}cout << dp[n] << endl;
}

http://www.dtcms.com/a/357292.html

相关文章:

  • 解锁Libvio访问异常:从故障到修复的全攻略
  • 从“Where”到“Where + What”:语义多目标跟踪(SMOT)全面解读
  • C# 日志写入loki
  • 海外广告流量套利:为什么需要使用移动代理IP?
  • 接吻数问题:从球体堆叠到高维空间的数学奥秘
  • 告别K8s部署繁琐!用KubeOperator可视化一键搭建生产级集群
  • 玄机靶场 | 冰蝎3.0-jsp流量分析
  • ACID分别如何实现
  • Dockerfile实现java容器构建及项目重启(公网和内网)
  • SOME/IP-SD IPv4组播的通信参数由谁指定?
  • React学习教程,从入门到精通, ReactJS - 特性:初学者的指南(4)
  • C++链表双杰:list与forward_list
  • ElasticSearch对比Solr
  • Node.js 的流(Stream)是什么?有哪些类型?
  • DQL单表查询相关函数
  • STM32F2/F4系列单片机解密和芯片应用介绍
  • Ubuntu虚拟机磁盘空间扩展指南
  • AI视频安防,为幼儿园安全保驾护航
  • 基于 GPT-OSS 的成人自考口语评测 API 开发全记录
  • 深度解密SWAT模型:遥感快速建模、DEM/LU/气象数据不确定性、子流域/坡度划分、未来土地利用与气候变化情景模拟及措施效益评估
  • 龙巍:探究青铜器在木雕中的运用
  • VS Code C#调试完全指南
  • [AI人脸替换] docs | 环境部署指南 | 用户界面解析
  • 红色视频剪辑制作——走进广州农讲所:在红墙黄瓦间感悟初心与传承
  • “游戏手柄”线性霍尔传感器IC替代方案:赛卓SC470X
  • Instance Normalization(实例归一化)
  • Stage应用模型及状态存储
  • 【Android 16】Android W 的冻结机制内核分析
  • 车载以太网通信测试:牢筑车载网络的质量防线
  • 【51单片机】【protues仿真】 基于51单片机叫号系统