梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
动态规划的本质:它是一种 “以空间换时间”的优化算法,核心是把复杂问题拆解为多个重叠的子问题,通过记录子问题的最优解(状态),避免重复计算,最终由子问题的最优解推导出原问题的最优解。
背包问题是动态规划的经典专项,属于带约束的最值 / 计数问题,核心场景是 “在有限的容量约束下,选择物品使价值最大 / 数量最多,或求合法选择的方案数”。根据物品的选择规则,分为三大经典类型,解题思路高度统一,是 DP 的必学专项。
| 背包类型 | 核心规则 | 典型问题 | 状态定义(二维) |
|---|---|---|---|
| 01 背包 | 每个物品只能选一次 | 选物品使总价值最大(容量有限) | dp[i][j]:前 i 个物品,容量 j 的最大价值 |
| 完全背包 | 每个物品可以选无限次 | 组合总和 IV(LeetCode 377) | dp[i][j]:前 i 个物品,和为 j 的方案数 |
| 多重背包 | 每个物品选择有次数限制 | 选物品(有数量限制)使价值最大 | dp[i][j]:前 i 个物品,容量 j 的最大价值 |
问题描述:
算法解析:
dp 数组计算过程:
输入:
#include <iostream>
#include <vector>
using namespace std;
int zeroOneKnapsack(int C, vector<int>& w, vector<int>& v) {
int n = w.size();
// 二维DP数组:dp[i][j] 前i个物品,容量j的最大价值
vector<vector<int>> dp(n + 1, vector<int>(C + 1, 0));
// 遍历每个物品(i从1到n,对应第i个物品)
for (int i = 1; i <= n; ++i) {
// 遍历所有可能的背包容量
for (int j = 1; j <= C; ++j) {
// 容量不足,无法选第i个物品
if (j < w[i-1]) {
dp[i][j] = dp[i-1][j];
} else {
// 容量足够,选或不选取最大值
dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i-1]] + v[i-1]);
}
}
}
// 输出DP数组(可选,帮助理解)
cout << "DP数组(行:物品数,列:容量):" << endl;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= C; ++j) {
printf("%3d",dp[i][j]);
}
cout << endl;
}
return dp[n][C];
}
int main() {
// 示例:背包容量C=8,物品重量[1,2,3,4],价值[5,6,7,8]
int C = 8;
vector<int> w = {1, 2, 3, 4};
vector<int> v = {5, 6, 7, 8};
int maxValue = zeroOneKnapsack(C, w, v);
cout << "背包最大价值:" << maxValue << endl;
return 0;
}
DP数组(行:物品数,列:容量):
0 0 0 0 0 0 0 0 0
0 5 5 5 5 5 5 5 5
0 5 6 11 11 11 11 11 11
0 5 6 11 12 13 18 18 18
0 5 6 11 12 13 18 19 20
背包最大价值:20
问题描述:
算法解析:
#include <iostream>
#include <vector>
#include <climits> // 用于 INT_MAX
#include <algorithm> // 用于 min 函数
using namespace std;
int coinChange(vector<int>& coins, int amount) {
// 定义dp数组,dp[i]表示凑成金额i所需的最少硬币数
// 初始化大小为amount+1,值为一个极大值(表示初始不可达)
vector<int> dp(amount + 1, INT_MAX);
// 基准条件:凑成0元需要0枚硬币
dp[0] = 0;
// 遍历每个金额,从1到目标金额
for (int i = 1; i <= amount; ++i) {
// 遍历每种硬币面额
for (int coin : coins) {
// 条件1:当前硬币面额不超过当前金额
// 条件2:dp[i - coin]不是初始的极大值(说明i - coin金额是可达的)
if (coin <= i && dp[i - coin] != INT_MAX) {
// 状态转移:取当前dp[i]和「凑i-coin的硬币数+1」的最小值
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
}
// 如果dp[amount]还是极大值,说明无法凑成,返回-1;否则返回结果
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
int main() {
// 测试用例1:[1,2,5] 凑11元,预期输出3
vector<int> coins1 = {1, 2, 5};
cout << coinChange(coins1, 11) << endl;
// 测试用例2:[2] 凑3元,预期输出-1
vector<int> coins2 = {2};
cout << coinChange(coins2, 3) << endl;
// 测试用例3:[1] 凑0元,预期输出0
vector<int> coins3 = {1};
cout << coinChange(coins3, 0) << endl;
return 0;
}
3
-1
0
问题描述:
算法解析:
#include <iostream>
#include <vector>
using namespace std;
int change(int amount, vector<int>& coins) {
// 初始化dp数组,dp[i]表示凑成金额i的组合数
vector<int> dp(amount + 1, 0);
// 基准条件:凑成0元只有1种方式(不选任何硬币)
dp[0] = 1;
// 第一步:遍历每一种硬币(先遍历硬币保证组合不考虑顺序)
for (int coin : coins) {
// 第二步:遍历金额,从coin到amount(完全背包:物品可重复选,正序遍历)
for (int i = coin; i <= amount; ++i) {
// 状态转移:累加「不使用当前硬币的组合数」和「使用当前硬币的组合数」
dp[i] += dp[i - coin];
}
}
// dp[amount]即为凑成目标金额的组合总数
return dp[amount];
}
int main() {
// 测试用例1:amount=5, coins=[1,2,5],预期输出4(5;2+2+1;2+1+1+1;1*5)
vector<int> coins1 = {1, 2, 5};
cout << change(5, coins1) << endl;
// 测试用例2:amount=3, coins=[2],预期输出0(无法凑成)
vector<int> coins2 = {2};
cout << change(3, coins2) << endl;
// 测试用例3:amount=0, coins=[1],预期输出1(空组合)
vector<int> coins3 = {1};
cout << change(0, coins3) << endl;
return 0;
}
4
0
1