前言
完全背包问题 是一种经典的动态规划问题,通常用于求解优化问题。在这个问题中,我们有一个背包和一组物品,每种物品有一个特定的重量和价值。
- 与01背包问题不同的是,在完全背包问题中,每种物品可以无限次使用。
问题描述
给定一个背包容量 W
和 n
种物品,每种物品 i
具有重量 w[i]
和价值 v[i]
。我们希望在不超过背包容量的情况下,选择物品使得背包中物品的总价值最大化。
动态规划解法
动态规划解法通过构建一个状态转移表来解决这个问题。我们可以使用一个一维数组 dp
来表示最大价值,其中 dp[j]
表示背包容量为 j
时可以获得的最大价值。
状态转移方程:
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
其中 j
是当前背包容量,w[i]
和 v[i]
分别是第 i
种物品的重量和价值。这个方程的意思是,对于每个容量 j
,我们可以选择不选择第 i
种物品,或者选择第 i
种物品,取这两者中的最大值。
算法题
1.【模板】完全背包
思路
-
数据读入:
- 读入物品数量
n
和背包容量V
。 - 读入每种物品的价值
v[i]
和重量w[i]
。
- 读入物品数量
-
问题一:完全背包问题
- 使用二维数组
dp[i][j]
来表示前i
个物品中背包容量为j
时的最大价值。 - 初始化
dp
数组,dp[i][j]
继承自dp[i-1][j]
(不选当前物品)。 - 如果当前背包容量
j
能容纳第i
种物品,则更新dp[i][j]
为选用当前物品后的最大价值。
- 使用二维数组
-
问题二:判断背包是否有解的完全背包问题
- 初始化
dp
数组,设置dp[0][j]
为 -1(表示不可能达到这些容量)。 - 对于每种物品,更新
dp[i][j]
,但前提是当前容量j
可以通过当前物品达成并且之前的状态不为 -1。
- 初始化
-
输出结果:
- 对于问题一,输出最大价值。
- 对于问题二,输出背包容量
V
时的最大价值,如果为 -1 则输出 0(表示无法达到这个容量)。
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];
int main() {
// 读入数据
cin >> n >> V;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// 问题一
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= V; ++j) {
dp[i][j] = dp[i - 1][j];
if (j >= v[i])
dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
cout << dp[n][V] << endl;
// 问题二
memset(dp, 0, sizeof(dp));
for (int j = 1; j <= V; ++j)
dp[0][j] = -1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= V; ++j) {
dp[i][j] = dp[i - 1][j];
if (j >= v[i] && dp[i][j - v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
2.零钱兑换
思路
-
初始化:
INF
定义为一个很大的值,用于表示无法达成的情况。dp[i][j]
表示使用前i
种硬币组合,总金额j
所需的最少硬币数。
-
DP 数组初始化:
dp[0][j]
初始化为INF
(表示使用 0 种硬币无法达到金额j
,除非j
为 0)。dp[0][0]
被隐式地设置为 0。
-
状态转移:
- 对于每个硬币
i
和金额j
,有两种选择:- 不使用当前硬币
i
:即dp[i][j]
继承自dp[i-1][j]
。 - 使用当前硬币
i
:更新dp[i][j]
为dp[i][j - coins[i-1]] + 1
(即在不使用当前硬币时的最小硬币数基础上加上当前硬币)。
- 不使用当前硬币
- 比较这两种情况,取最小值。
- 对于每个硬币
-
返回结果:
- 如果
dp[n][amount]
大于等于INF
,说明无法用给定的硬币组合成目标金额amount
,返回 -1。 - 否则,返回
dp[n][amount]
,即最少硬币数。
- 如果
-
总结:
dp[i][j]
:使用前i
种硬币时,凑出金额j
的最小硬币数。- 时间复杂度:
O(n * amount)
,n
是硬币种类数,amount
是目标金额。 - 空间复杂度:
O(n * amount)
。
代码
class Solution {
public:
const int INF = 0x3f3f3f3f;
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
// 创建dp数组
// dp[i][j]: 在前i个数中,选择硬币,使总金额恰好为j的最少硬币个数
vector<vector<int>> dp(n+1, vector<int>(amount+1));
// 初始化
afor(int j = 1; j <= amount; ++j)
dp[0][j] = INF;
for(int i = 1; i <= n; ++i)
for(int j = 0; j <= amount; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i-1])
dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);
}
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
};
3.零钱兑换II
思路
-
初始化:
dp[j]
表示凑成金额j
的不同组合数。dp[0] = 1
表示凑成金额 0 的组合数为 1(即不选任何硬币)。
-
状态转移:
- 对于每种硬币
coins[i-1]
,更新dp[j]
。这里需要注意的是,我们从金额coins[i-1]
开始更新,因为金额小于coins[i-1]
的情况不会受到当前硬币影响。 dp[j] += dp[j - coins[i-1]]
:这是因为dp[j - coins[i-1]]
代表了凑成金额j - coins[i-1]
的组合数,而dp[j]
的更新表示将当前硬币coins[i-1]
加入这些组合中,得到新的组合数。
- 对于每种硬币
-
返回结果:
dp[amount]
存储了凑成金额amount
的所有可能组合数。
-
总结:
- 时间复杂度:
O(n * amount)
,其中n
是硬币的种类数,amount
是目标金额。每种硬币遍历amount
次。 - 空间复杂度:
O(amount)
。我们只使用了一维的dp
数组来存储状态,减少了空间复杂度。
- 时间复杂度:
代码
class Solution {
public:
int change(int amount, vector<int>& coins) {
// 空间优化版本:
int n = coins.size();
// 创建dp数组
// dp[i][j]: 从前i个位选,使其总和为i,的选法
vector<int> dp(amount+1);
dp[0] = 1;
for(int i = 1; i <= n; ++i)
for(int j = coins[i-1]; j <= amount; ++j)
dp[j] += dp[j-coins[i-1]];
return dp[amount];
}
};
4.完全平方数
思路
-
初始化:
int _sqrt = sqrt(n);
计算不超过n
的最大整数平方根。vector<vector<int>> dp(_sqrt + 1, vector<int>(n + 1));
创建二维dp
数组,其中dp[i][j]
代表前i
个平方数中组成j
的最少数量。for (int j = 1; j <= n; ++j) dp[0][j] = 0x3f3f3f3f;
初始化dp
表中的不可达状态为一个很大的值(表示初始状态下无法组成j
)。
-
填表:
for (int i = 1; i <= _sqrt; ++i)
遍历每个可能的平方数。dp[i][j] = dp[i-1][j];
初始状态为不选第i
个平方数的情况下的结果。if (j >= i * i)
当j
大于等于当前平方数i*i
时,尝试使用这个平方数更新dp
。dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
更新dp[i][j]
为包括当前平方数的最小值。
-
返回结果:
return dp[_sqrt][n];
最终结果在dp[_sqrt][n]
中,它表示用最少的平方数组合成n
。
-
总结:
- 时间复杂度:
O(sqrt(n) * n)
,其中sqrt(n)
是平方数的数量,n
是目标值。 - 空间复杂度:
O(sqrt(n) * n)
,由于使用了二维dp
数组。
代码
class Solution {
public:
int numSquares(int n) {
int _sqrt = sqrt(n);
// 创建dp数组
// dp[i][j]: 在前i个数中,选择数使其和等于j,时的最少数量
vector<vector<int>> dp(_sqrt+1, vector<int>(n+1));
for(int j = 1; j <= n; ++j) dp[0][j] = 0x3f3f3f3f;
// 填表
for(int i = 1; i <= _sqrt; ++i)
for(int j = 1; j <= n; ++j)
{
dp[i][j] = dp[i-1][j]; // 不选i位置数
if(j >= i*i)
dp[i][j] = min(dp[i][j], dp[i][j-i*i]+1);
}
return dp[_sqrt][n];
}
};
思路
代码
思路
代码