文章目录
博客主页:lyyyyyrics
🚀完全背包问题
🛸1. 引言
动态规划(DP)是算法中的重要技术,背包问题则是其中的经典问题之一。本篇博客将介绍完全背包问题及其解决方案。
🛸2. 问题定义
🛰️完全背包问题
在完全背包问题中,每种物品有无限个可用,目标是在限定的背包容量内,选择物品使得总价值最大。
🛰️数学描述
给定n
种物品,每种物品有重量weight[i]
和价值value[i]
。背包容量为C
,求解在不超过容量C
的情况下,可以获得的最大价值。
🛸3. 动态规划思想
🛰️DP思想概述
动态规划通过将问题分解为子问题,利用子问题的解来构建最终解。完全背包问题具有最优子结构性质和重复子问题结构,非常适合用动态规划求解。
🛸4. 状态转移方程
🛰️方程推导
完全背包问题的公式推导如下:
我们有一个背包,容量为 V \text{V} V,并有 n n n 种物品,每种物品的重量分别为 w 1 , w 2 , . . . , w n w_1, w_2, ..., w_n w1,w2,...,wn,价值分别为 v 1 , v 2 , . . . , v n v_1, v_2, ..., v_n v1,v2,...,vn。
设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 种物品中,背包容量为 j j j 时能够获得的最大价值。
对于第 i i i 种物品,我们有两种选择:放入背包或不放入背包。
如果我们选择放入背包,那么背包容量减少 w i w_i wi,价值增加 v i v_i vi,即有 d p [ i ] [ j ] = d p [ i ] [ j − w i ] + v i dp[i][j] = dp[i][j-w_i] + v_i dp[i][j]=dp[i][j−wi]+vi。
如果我们选择不放入背包,那么背包容量不变,仍然有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i−1][j]。
综上所述,完全背包问题的状态转移方程为:
d p [ i ] [ j ] = max ( d p [ i ] [ j − w i ] + v i , d p [ i − 1 ] [ j ] ) dp[i][j] = \max(dp[i][j-w_i] + v_i, dp[i-1][j]) dp[i][j]=max(dp[i][j−wi]+vi,dp[i−1][j])
其中, 0 ≤ i ≤ n 0 \leq i \leq n 0≤i≤n, 0 ≤ j ≤ V 0 \leq j \leq \text{V} 0≤j≤V。
初始条件为 d p [ 0 ] [ j ] = 0 dp[0][j] = 0 dp[0][j]=0,表示没有任何物品可选时,背包的价值为 0; d p [ i ] [ 0 ] = 0 dp[i][0] = 0 dp[i][0]=0,表示背包容量为 0 时,无法放入任何物品,价值也为 0。
最终的答案为 d p [ n ] [ V ] dp[n][\text{V}] dp[n][V],表示在前 n n n 种物品中,背包容量为 V \text{V} V 时能够获得的最大价值。
🚀例题
🛸1.【模版】完全背包
题目:
样例输出和输入:
算法原理:
第一个问题:求背包不装满时,背包能装的最大价值。
状态表示:dp[i][j]表示前i个物品中,能选出的不超过容量j的最大价值。
状态转移方程:
推导出:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + 2 w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 3 w [ i ] , . . . . . . , + d p [ i − 1 ] [ j − k v [ i ] ] + k w [ i ] ) dp[i][j] = \max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2v[i]]+2w[i], dp[i-1][j-3v[i]]+3w[i], ......, +dp[i-1][j-kv[i]]+kw[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i],dp[i−1][j−2v[i]]+2w[i],dp[i−1][j−3v[i]]+3w[i],......,+dp[i−1][j−kv[i]]+kw[i])
可以得出:
d p [ i ] [ j − v [ i ] ] = max ( d p [ i − 1 ] [ j − v [ i ] ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 2 w [ i ] , . . . . . . . + d p [ i − 1 ] [ j − x v [ i ] ] + ( x − 1 ) w [ i ] ) dp[i][j-v[i]]=\max(dp[i-1][j-v[i]],dp[i-1][j-2v[i]]+w[i],dp[i-1][j-3v[i]]+2w[i],.......+dp[i-1][j-xv[i]]+(x-1)w[i]) dp[i][j−v[i]]=max(dp[i−1][j−v[i]],dp[i−1][j−2v[i]]+w[i],dp[i−1][j−3v[i]]+2w[i],.......+dp[i−1][j−xv[i]]+(x−1)w[i])
讨论下面式子中的最后一个x是否和上面的k相同,首先我们要知道,给定一个背包的容量,选择一个位置的值的极限是相同的,我们不能选择一个位置的值选择无穷多个,所以这里 j − k v [ i ] j-kv[i] j−kv[i]的极限和 j − x v [ i ] j-xv[i] j−xv[i]的极限是相同的,所以这里x和k应该相同,由于这两个相同,所以我们可以将第二个式子左右两边同时加上一个 w [ i ] w[i] w[i]可以得出:
d p [ i ] [ j − v [ i ] ] + w [ i ] = max ( d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + 2 w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 3 w [ i ] , . . . . . . . + d p [ i − 1 ] [ j − x v [ i ] ] + x w [ i ] ) dp[i][j-v[i]]+w[i]=\max(dp[i-1][j-v[i]]+w[i],dp[i-1][j-2v[i]]+2w[i],dp[i-1][j-3v[i]]+3w[i],.......+dp[i-1][j-xv[i]]+xw[i]) dp[i][j−v[i]]+w[i]=max(dp[i−1][j−v[i]]+w[i],dp[i−1][j−2v[i]]+2w[i],dp[i−1][j−3v[i]]+3w[i],.......+dp[i−1][j−xv[i]]+xw[i])
上面这个式子和第一个式子只差了一个 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]所以我们等效替换:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v [ i ] ] + w [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]) dp[i][j]=max(dp[i−1][j],dp[i][j−v[i]]+w[i])
所以最后:
状态转移方程就是:dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i])
,完全背包问题统一都是这个状态转移方程,基本上。
代码:
为优化:
cpp
#include <cstring>
#include<iostream>
#include<vector>
const int N = 1001;
int n, V;
int dp[N][N];
int v[N], w[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 = 1;j <= V;j++)
{
dp[i][j] = dp[i - 1][j];
if (j >= v[i])dp[i][j] = max(dp[i - 1][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 = 1;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 - 1][j], dp[i][j - v[i]] + w[i]);
}
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
滚动数组优化后的代码:
cpp
#include <cstring>
#include<iostream>
#include<vector>
using namespace std;
const int N = 1001;
int n, V;
int dp[N];
int v[N], w[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 = v[i];j <= V;j++)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[V] << endl;
memset(dp, 0, sizeof dp);
for (int j = 1;j <= V;j++)dp[j] = -1;
for (int i = 1;i <= n;i++)
for (int j = v[i];j <= V;j++)
if (dp[j - v[i]] != -1)dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}
运行结果:
🛸2.零钱兑换
题目:
样例输出和输入:
这道题首先需要注意到无限 ,无限这个词就表示这道题很可能是背包问题中的完全背包问题,这道题很显然也是。
算法原理:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i位置之前的所有数中的组合能凑出来是 a m o u n t amount amount的最小的组合的硬币个数。
状态转移方程:
很显然这道题的状态转移方程是:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − c o i n s [ i ] ] + 1 ) dp[i][j]=max(dp[i-1][j],dp[i][j-coins[i]]+1) dp[i][j]=max(dp[i−1][j],dp[i][j−coins[i]]+1)
代码:
未优化的代码:
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
for (int j = 1; j <= amount; j++)
dp[0][j] = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1])
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
}
}
return dp[n][amount] == 0x3f3f3f3f ? -1 : dp[n][amount];
}
};
优化过后的代码:
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
int INF = 0x3f3f3f3f;
vector <int> dp(amount + 1, INF);
dp[0] = 0;
for (int i = 1;i <= n;i++)
for (int j = coins[i - 1];j <= amount;j++)
dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
return dp[amount] == INF ? -1 : dp[amount];
}
};
运行结果:
🛸3.零钱兑换Ⅱ
题目:
样例输出和输入:
算法原理:
这道题和上一道题基本上是一样的:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个位置的硬币中能凑成 a m o u n t amount amount的方法总数。
状态转移方程:
第一种状态:不选择 i i i位置-> d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]
第二种状态:选择 i i i位置-> d p [ i ] [ j − c o i n s [ i ] ] dp[i][j-coins[i]] dp[i][j−coins[i]]
总的方法数就是这两个之和:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − c o i n s [ i ] ] dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]] dp[i][j]=dp[i−1][j]+dp[i][j−coins[i]]
代码:
未优化的代码:
cpp
class Solution {
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
for (int i = 0;i <= n;i++)
{
dp[i][0] = 1;
}
for (int i = 1;i <= n;i++)
{
for (int j = 1;j <= amount;j++)
{
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1])dp[i][j] += dp[i][j - coins[i - 1]];
}
}
return dp[n][amount];
}
};
优化过后的代码:
cpp
class Solution {
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
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.完全平方数
题目:
样例输出和输入:
算法原理:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前i个数中的完全平方和数之和能等于n的最少的那个组合的个数。
状态转移方程:
第一种状态:不选择 i i i位置 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]
第二种状态:选择 i i i位置 d p [ i ] [ j − i ∗ i ] + 1 dp[i][j-i*i]+1 dp[i][j−i∗i]+1
最后两个状态中的最少的那个:
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − i ∗ i ] ) dp[i][j]=min(dp[i-1][j],dp[i][j-i*i]) dp[i][j]=min(dp[i−1][j],dp[i][j−i∗i])
代码:
cpp
class Solution {
public:
int numSquares(int n)
{
int m = sqrt(n);
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
int INF = 0x3f3f3f3f;
for (int j = 1;j <= n;j++)
{
dp[0][j] = INF;
}
for (int i = 1;i <= m;i++)
{
for (int j = 1;j <= n;j++)
{
dp[i][j] = dp[i - 1][j];
if (j >= i * i)dp[i][j] = min(dp[i - 1][j], dp[i][j - i * i] + 1);
}
}
return dp[m][n];
}
};
优化:
cpp
class Solution {
public:
int numSquares(int n)
{
int m = sqrt(n);
int INF = 0x3f3f3f3f;
vector<int> dp(n + 1,INF);
dp[0] = 0;
for (int i = 1;i <= m;i++)
for (int j = i * i;j <= n;j++)
dp[j] = min(dp[j], dp[j - i * i] + 1);
return dp[n];
}
};
运行结果:
🚀总结
通过对完全背包问题的深入探讨,我们了解了动态规划在解决这类问题中的重要性。完全背包问题在实际应用中非常广泛,例如货币兑换、资源分配和路径规划等。在解决过程中,我们学会了如何定义状态、确定状态转移方程,并通过优化空间复杂度提升算法效率。
关键在于,理解并掌握动态规划的核心思想,能够帮助我们从容应对各种复杂的优化问题。希望通过本文的介绍,大家对完全背包问题有了更清晰的理解,并能将其应用到实际问题中去。
未来的学习中,我们可以尝试更多变体问题的解决方法,不断拓展自己的算法知识。祝大家在学习动态规划的道路上取得更大进步!