文章目录
- 前言
- 问题描述
- 动态规划解法
- 算法题
- 1.【模板】完全背包
- 2.零钱兑换
- 3.零钱兑换II
- 4.完全平方数
前言
完全背包问题 是一种经典的动态规划问题,通常用于求解优化问题。在这个问题中,我们有一个背包和一组物品,每种物品有一个特定的重量和价值。
- 与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];
}
};
思路
代码
思路
代码



















