目录
一、什么是背包问题?
基本概念
二、背包问题的常见类型
1. 0-1背包问题
2. 完全背包问题
3. 多重背包问题
4. 分数背包问题
三、0-1背包问题的动态规划解法
1. 基本思路
2. C++实现代码
3. 空间优化版本
四、完全背包问题的解法
1. 基本思路
2. C++实现代码
五、背包问题的实际应用
六、经典例题与解答
例题1:分割等和子集(LeetCode 416)
例题2:目标和(LeetCode 494)
七、背包问题的优化技巧
八、总结
背包问题是计算机科学和运筹学中最经典的优化问题之一,也是动态规划算法的典型应用场景。本文将全面介绍背包问题的各种变体、解决思路以及实际应用。
背包问题(Knapsack Problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,如何选择物品使得总价值最大。
物品集合:每个物品有重量w和价值v
背包容量:总重量限制W
目标:选择物品装入背包,使总重量不超过W且总价值最大
每个物品要么完整选取(1),要么完全不选(0),不能分割。
示例:
背包容量为4kg,物品有:
重量1kg,价值15元
重量2kg,价值20元
重量3kg,价值30元
每种物品可以选取无限次。
每种物品有数量限制,可以选取多次但不能超过限制。
物品可以分割,可以选取部分物品。
使用二维数组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-1][j] (j < w[i])
#include
#include
#include
using namespace std;
int knapsack01(int W, const vector& weights, const vector& values) {
int n = weights.size();
vector> dp(n + 1, vector(W + 1, 0));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= W; ++j) {
if (weights[i-1] <= j) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][W];
}
int main() {
vector values = {60, 100, 120};
vector weights = {10, 20, 30};
int W = 50;
cout << "最大价值为: " << knapsack01(W, weights, values) << endl;
return 0;
}
可以优化为一维数组实现:
int knapsack01_optimized(int W, const vector& weights, const vector& values) {
int n = weights.size();
vector dp(W + 1, 0);
for (int i = 0; i < n; ++i) {
for (int j = W; j >= weights[i]; --j) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[W];
}
与0-1背包类似,但物品可以重复选取。
状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]) (j >= w[i])
= dp[i-1][j] (j < w[i])
int completeKnapsack(int W, const vector& weights, const vector& values) {
int n = weights.size();
vector dp(W + 1, 0);
for (int i = 0; i < n; ++i) {
for (int j = weights[i]; j <= W; ++j) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[W];
}
资源分配:有限的预算下选择最有价值的项目组合
投资组合:在风险限制下最大化收益
货物装载:卡车或集装箱的装载优化
任务调度:有限时间内完成最有价值的任务组合
数据压缩:选择最有价值的数据块进行存储
问题描述:给定一个只包含正整数的非空数组,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
分析:这实际上是求是否存在子集的和等于总和的一半,可以转化为背包问题。
C++解法:
bool canPartition(vector& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 != 0) return false;
int target = sum / 2;
vector dp(target + 1, false);
dp[0] = true;
for (int num : nums) {
for (int j = target; j >= num; --j) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
问题描述:给定一个非负整数数组和一个目标数S,你有可以在每个数字前添加+或-,求共有多少种组合使得结果为S。
分析:可以转化为背包问题,寻找子集满足(sum - S)/2的差。
C++解法:
int findTargetSumWays(vector& nums, int S) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum < S || (sum + S) % 2 != 0) return 0;
int target = (sum + S) / 2;
vector dp(target + 1, 0);
dp[0] = 1;
for (int num : nums) {
for (int j = target; j >= num; --j) {
dp[j] += dp[j - num];
}
}
return dp[target];
}
滚动数组优化:将二维DP降为一维,节省空间
提前终止:当找到最优解时可提前结束
物品预处理:按单位价值排序,优先考虑高价值物品
分支限界法:对于大容量问题,结合贪心算法进行剪枝
背包问题是动态规划的经典应用,掌握它对于理解算法设计思想至关重要。不同类型的背包问题有不同的状态转移方程,但核心思想都是通过构建状态表来记录最优解。实际应用中,需要根据具体问题选择合适的变体和优化方法。
通过本文的学习,你应该已经掌握了:
背包问题的基本概念和分类
0-1背包和完全背包的动态规划解法
背包问题的空间优化技巧
背包问题在实际问题中的应用
相关LeetCode题目的解法
希望这篇博客能帮助你深入理解背包问题,并在算法学习和编程实践中灵活运用。