C 语言与 C++ 实现背包问题:从零到精通
ALGORITHM · 动态规划
背包问题:从零到精通
C 语言与 C++ 实现 · 0/1背包 · 完全背包 · 多重背包
一、什么是背包问题?
背包问题(Knapsack Problem)是组合优化领域最经典的问题之一,也是学习动态规划(Dynamic Programming)的入门首选。
问题描述:给定 n 个物品,每个物品有重量 w[i] 和价值 v[i],以及一个容量为 W 的背包。选择若干物品装入背包,使得在总重量不超过 W 的前提下,总价值最大。
背包问题的分类
|
类型 |
每个物品可选次数 |
典型问题 |
|
0/1 背包 |
最多选 1 次 |
投资组合选择 |
|
完全背包 |
无限次 |
硬币找零 |
|
多重背包 |
有限次 c[i] |
仓库库存分配 |
|
分组背包 |
每组选 1 个 |
课程选择 |
本文将使用 C 语言和 C++ 分别实现 0/1 背包、完全背包和多重背包三种变体,帮助你全面理解背包问题的核心思想。
二、0/1 背包问题
2.1 问题分析
定义状态 dp[i][j]:考虑前 i 个物品,背包容量为 j 时的最大价值。
状态转移方程:
|
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) (j >= w[i]) dp[i][j] = dp[i-1][j] (j < w[i]) |
2.2 C 语言实现(二维数组)
📝 C 语言 · 0/1 背包 · 二维 DP
#include <stdio.h>
// 定义最大物品数量和背包最大容量
#define MAXN 1005
#define MAXW 10005
// 求两个整数中较大值的宏/函数
int max(int a, int b) {
return a > b ? a : b;
}
int main(void) {
int n, W;
int w[MAXN], v[MAXN];
// dp[i][j] 表示前 i 个物品放入容量为 j 的背包中所能获得的最大价值
int dp[MAXN][MAXW] = {0};
// 读取物品数量 n 和背包容量 W
scanf("%d %d", &n, &W);
// 读取每个物品的重量 w[i] 和价值 v[i]
for (int i = 1; i <= n; i++) {
scanf("%d %d", &w[i], &v[i]);
}
// 动态规划状态转移过程
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j >= w[i]) {
// 如果当前背包容量 j 能够装下第 i 个物品:
// 比较“不装第 i 个物品”和“装第 i 个物品”两种情况,取最大值
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
} else {
// 如果当前背包容量 j 装不下第 i 个物品:
// 最大价值等于前 i-1 个物品在容量 j 下的最大价值
dp[i][j] = dp[i-1][j];
}
}
}
// 输出最终结果:前 n 个物品放入容量为 W 的背包的最大价值
printf("%d\n", dp[n][W]);
return 0;
}
2.3 C++ 实现(滚动数组优化)
二维数组 dp[i][j] 只依赖上一行 dp[i-1][*],因此可以用一维数组从后往前更新,空间从 O(nW) 降到 O(W)。
📝 C++ · 0/1 背包 · 一维滚动数组
#include <iostream>
#include <vector>
#include <algorithm> // 用于 max 函数
using namespace std;
int main() {
int n, W;
// 读取物品数量 n 和背包总容量 W
cin >> n >> W;
// 使用 vector 存储物品的重量 w 和价值 v,索引从 1 开始
vector<int> w(n + 1), v(n + 1);
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
// 初始化为 0,表示没有任何物品时,任何容量的最大价值都是 0
vector<int> dp(W + 1, 0);
// 外层循环:遍历每一个物品
for (int i = 1; i <= n; i++) {
// 内层循环:逆序遍历背包容量(从最大容量 W 递减到当前物品的重量 w[i])
// 逆序遍历是为了保证每个物品最多只被放入背包一次
for (int j = W; j >= w[i]; j--) {
// 状态转移方程:
// 比较“不放入当前物品”和“放入当前物品”两种情况,取最大值
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
// 输出最终结果:容量为 W 的背包能装下的最大价值
cout << dp[W] << endl;
return 0;
}
⚠️ 关键点:0/1 背包的一维优化必须从后往前遍历 j,否则会重复使用同一个物品(变成完全背包)。
三、完全背包问题
3.1 问题分析
与 0/1 背包的区别:每个物品可以选无限次。状态转移方程变为:
|
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]) (j >= w[i]) |
注意这里是 dp[i][j-w[i]] 而不是 dp[i-1][j-w[i]],因为同一物品可以重复选取。一维优化时,只需把内层循环改为正序遍历。
3.2 C 语言实现
📝 C 语言 · 完全背包 · 一维优化
#include <stdio.h>
// 定义最大物品数量和背包最大容量
#define MAXN 1005
#define MAXW 10005
// 求两个整数中较大值的宏/函数
int max(int a, int b) {
return a > b ? a : b;
}
int main(void) {
int n, W;
int w[MAXN], v[MAXN];
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
// 初始化为 0,表示没有任何物品时,任何容量的最大价值都是 0
int dp[MAXW] = {0};
// 读取物品数量 n 和背包总容量 W
scanf("%d %d", &n, &W);
// 读取每个物品的重量 w[i] 和价值 v[i]
for (int i = 1; i <= n; i++) {
scanf("%d %d", &w[i], &v[i]);
}
// 动态规划状态转移过程
for (int i = 1; i <= n; i++) {
// 【核心逻辑】:正序遍历背包容量(从当前物品的重量 w[i] 递增到最大容量 W)
// 正序遍历保证了在计算 dp[j] 时,dp[j - w[i]] 已经在当前轮次被更新过。
// 这意味着 dp[j - w[i]] 中可能已经包含了当前物品 i,从而允许同一物品被多次放入背包
for (int j = w[i]; j <= W; j++) {
// 状态转移方程:
// 比较“不放入当前物品”和“放入当前物品”两种情况,取最大值
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
// 输出最终结果:容量为 W 的背包能装下的最大价值
printf("%d\n", dp[W]);
return 0;
}
3.3 C++ 实现
📝 C++ · 完全背包
#include <iostream>
#include <vector>
#include <algorithm> // 用于 max 函数
using namespace std;
int main() {
int n, W;
// 读取物品数量 n 和背包总容量 W
cin >> n >> W;
// 使用 vector 存储物品的重量 w 和价值 v,索引从 1 开始
vector<int> w(n + 1), v(n + 1);
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
// 初始化为 0,表示没有任何物品时,任何容量的最大价值都是 0
vector<int> dp(W + 1, 0);
// 外层循环:遍历每一个物品
for (int i = 1; i <= n; i++) {
// 【核心逻辑】:正序遍历背包容量(从当前物品的重量 w[i] 递增到最大容量 W)
// 正序遍历保证了在计算 dp[j] 时,dp[j - w[i]] 已经在当前轮次被更新过。
// 这意味着 dp[j - w[i]] 中可能已经包含了当前物品 i,从而允许同一物品被多次放入背包。
for (int j = w[i]; j <= W; j++) {
// 状态转移方程:
// 比较“不放入当前物品”和“放入当前物品”两种情况,取最大值
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
// 输出最终结果:容量为 W 的背包能装下的最大价值
cout << dp[W] << endl;
return 0;
}
四、多重背包问题
4.1 问题分析
每个物品有数量上限 c[i]。朴素做法是把每件物品展开成 c[i] 个 0/1 背包物品,复杂度 O(nW * c[i])。
更优的做法是二进制拆分:把 c[i] 拆成 1, 2, 4, ..., 2^k, remainder 份,转化为 O(log c[i]) 个 0/1 背包物品。
4.2 C++ 实现(二进制拆分)
📝 C++ · 多重背包 · 二进制拆分
#include <iostream>
#include <vector>
#include <algorithm> // 用于 max 函数
using namespace std;
// 定义物品结构体,包含重量 w 和价值 v
struct Item {
int w, v;
};
int main() {
int n, W;
// 读取物品种类数 n 和背包总容量 W
cin >> n >> W;
// 用于存储二进制拆分后的新物品
vector<Item> items;
for (int i = 0; i < n; i++) {
int w, v, c;
// 读取当前物品的重量 w、价值 v 和数量限制 c
cin >> w >> v >> c;
// 【核心逻辑】:二进制拆分
// 将数量 c 拆分为 1, 2, 4, ..., 2^k, 剩余量 的组合
int k = 1;
while (k <= c) {
// 将拆分出的组合包作为一个独立的新物品存入数组
items.push_back({w * k, v * k});
c -= k; // 减去已拆分的数量
k *= 2; // 翻倍,准备拆分下一个 2 的幂次
}
// 如果最后还有剩余数量,将其单独作为一个新物品存入
if (c > 0) {
items.push_back({w * c, v * c});
}
}
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
vector<int> dp(W + 1, 0);
// 将拆分后的物品视为 0/1 背包问题进行求解
for (auto& it : items) {
// 【核心逻辑】:逆序遍历背包容量
// 保证每个“组合包”最多被放入背包一次,符合 0/1 背包的特性
for (int j = W; j >= it.w; j--) {
// 状态转移方程:比较“不放入”和“放入当前组合包”两种情况,取最大值
dp[j] = max(dp[j], dp[j - it.w] + it.v);
}
}
// 输出最终结果:容量为 W 的背包能装下的最大价值
cout << dp[W] << endl;
return 0;
}
4.3 C 语言实现(朴素方法)
📝 C 语言 · 多重背包 · 朴素方法
#include <stdio.h>
// 定义背包最大容量
#define MAXW 10005
// 求两个整数中较大值的宏/函数
int max(int a, int b) {
return a > b ? a : b;
}
int main(void) {
int n, W;
// 读取物品种类数 n 和背包总容量 W
scanf("%d %d", &n, &W);
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
int dp[MAXW] = {0};
// 外层循环:遍历每一种物品
for (int i = 0; i < n; i++) {
int w, v, c;
// 读取当前物品的重量 w、价值 v 和数量限制 c
scanf("%d %d %d", &w, &v, &c);
// 内层循环:逆序遍历背包容量(保证每个物品在本次决策中只被考虑一次)
for (int j = W; j >= w; j--) {
// 最内层循环:枚举当前物品选取的数量 k(从 1 到 c,且不能超过当前容量 j)
for (int k = 1; k <= c && k * w <= j; k++) {
// 状态转移方程:
// 比较“不放入更多当前物品”和“放入 k 个当前物品”两种情况,取最大值
dp[j] = max(dp[j], dp[j - k * w] + k * v);
}
}
}
// 输出最终结果:容量为 W 的背包能装下的最大价值
printf("%d\n", dp[W]);
return 0;
}
五、复杂度对比与选择建议
|
问题类型 |
时间复杂度 |
空间复杂度 |
优化技巧 |
|
0/1 背包 |
O(nW) |
O(W) 滚动数组 |
逆序遍历 j |
|
完全背包 |
O(nW) |
O(W) |
正序遍历 j |
|
多重背包(朴素) |
O(nW * c) |
O(W) |
三重循环 |
|
多重背包(二进制拆分) |
O(nW * log c) |
O(W + n log c) |
拆分为 log c 个物品 |
选择建议:
- 0/1 背包:逆序遍历 j,空间一维优化即可。
- 完全背包:正序遍历 j,与 0/1 背包唯一区别。
- 多重背包:数据量小时朴素可行;数据量大时必须用二进制拆分或单调队列优化。
- C 语言实现需手动管理数组大小;C++ 使用 vector 更灵活安全。
六、经典例题实战
例题:洛谷 P1048 采药
题目:辰辰有 M 分钟,共有 n 株草药,每株需要时间 t[i] 采摘,价值 v[i]。求最大价值。
这是一道标准的 0/1 背包问题。将 M 视为背包容量,每株草药就是一个物品。
📝 C++ · P1048 采药 · 完整解答
#include <iostream>
#include <vector>
#include <algorithm> // 用于 max 函数
using namespace std;
int main() {
int M, n;
// 读取背包总容量 M 和物品数量 n
cin >> M >> n;
// 使用 vector 存储物品的重量 t 和价值 v,索引从 1 开始
vector<int> t(n + 1), v(n + 1);
for (int i = 1; i <= n; i++) {
cin >> t[i] >> v[i];
}
// 一维滚动数组:dp[j] 表示背包容量为 j 时所能获得的最大价值
// 初始化为 0,表示没有任何物品时,任何容量的最大价值都是 0
vector<int> dp(M + 1, 0);
// 外层循环:遍历每一个物品
for (int i = 1; i <= n; i++) {
// 内层循环:逆序遍历背包容量(从最大容量 M 递减到当前物品的重量 t[i])
// 逆序遍历是为了保证在计算 dp[j] 时,dp[j - t[i]] 仍然是上一轮(未考虑当前物品)的状态,
// 从而确保每个物品最多只被放入背包一次。
for (int j = M; j >= t[i]; j--) {
// 状态转移方程:
// 比较“不放入当前物品”和“放入当前物品”两种情况,取最大值
dp[j] = max(dp[j], dp[j - t[i]] + v[i]);
}
}
// 输出最终结果:容量为 M 的背包能装下的最大价值
cout << dp[M] << endl;
return 0;
}
七、常见错误与调试技巧
|
错误现象 |
原因 |
解决方法 |
|
答案偏大 |
0/1 背包正序遍历 j |
改为逆序:for(j=W; j>=w[i]; j--) |
|
答案偏小 |
完全背包逆序遍历 j |
改为正序:for(j=w[i]; j<=W; j++) |
|
数组越界 |
j-w[i] < 0 |
加条件判断 j >= w[i] |
|
MLE 内存超限 |
二维数组过大 |
改用一维滚动数组 |
|
TLE 超时 |
多重背包朴素三重循环 |
使用二进制拆分或单调队列 |
八、总结与进阶
背包问题是动态规划的基石,掌握它之后可以触类旁通:
- 0/1 背包 → 子集和问题、分割等和子集
- 完全背包 → 零钱兑换、爬楼梯变种
- 多重背包 → 单调队列优化(P1776)
- 分组背包 → 依赖背包、树形背包
- 混合背包 → 组合多种背包类型
建议练习平台:
- 洛谷(luogu.com.cn):P1048 采药、P1060 开心的金明、P1776 宝物筛选
- LeetCode:322 零钱兑换、416 分割等和子集、474 一和零
- AcWing:2-01背包问题、3-完全背包问题、4-多重背包问题
— END —
感谢阅读,如有疑问欢迎留言交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)