开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
动态规划
动态规划是一类非常重要的算法,包含线性DP、树形DP、博弈DP、数位DP、状压DP等多种子类别,该算法通过寻找集合之间的递推关系求解问题,对应的算法题目难度普遍较大
背包问题是DP的基础,对于锻炼我们分析DP的能力有很大的帮助
01背包问题
特点: N种物品装背包,每个物品都有价值,背包容量为V,每种物品只能用一次,求最大可装入的价值
状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值
初始条件: f[0] [0]=0
状态转移方程: 如果已经装不下,则:f[i] [j]=f[i-1] [j] ;如果可以装下,那么在选第i个和不选之间取最大值即可,方程如下:
代码实现:
#include<iostream>
using namespace std;
const int N=1100;
int v[N],w[N];//分别存入体积和价值
int n,m; //分别表示物品个数和背包总容量
int dp[N][N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(j<v[i]){
dp[i][j]=dp[i-1][j];
}
else{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
空间优化: 代码在空间复杂度上还有优化的空间,由于我们知道新的状态仅与上一次的状态有关,我们可以把代码优化成下面的样子:
#include<iostream>
using namespace std;
const int N=1100;
int n,m; //分别表示物品个数和背包总容量
int dp[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int v,w;
cin>>v>>w;
for(int j=m;j>=v;j--){
dp[j]=max(/*由于上一次的状态未更新,相当于dp[i-1][j]*/dp[j],dp[j-v]+w);
}
}
cout<<dp[m]<<endl;
return 0;
}
完全背包问题
特点: 还是N种物品,装容量为V的背包,但是每种物品可以用无限次,仍然求最大价值
状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值
状态转移方程: 我们此时考虑第i个物品选k个对应的情况,从中取最大即可,则方程如下:
代码实现:
此方法可能会TLE
#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int dp[N][N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=0;k*v[i]<=j;k++){
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
优化: 我们关注下面的式子
由上两式,可得出如下递推关系: f [ i ] [ j ]=max(f[i] [j-v]+w , f [ i -1 ] [ j ])
于是我们的代码就可以优化成下面这样:
#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int dp[N][N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j] = dp[i-1][j];
if(j-v[i]>=0){
dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
空间优化: 仿照01背包的思路,我们可以将代码优化成下面这样
#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int dp[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int v,w;
cin>>v>>w;
for(int j=v;j<=m;j++){
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<<dp[m]<<endl;
return 0;
}
多重背包问题(暴力)
特点: 还是N种物品,V的总容量,但是每个物品个数有上限
状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值
状态转移方程: 我们自然就想到,完全可以沿用完全背包的第一种思路,则状态转移方程不变
代码实现:
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int dp[N][N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i]>>s[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=0;k<=s[i];k++){
if(k*v[i]<=j){
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
空间优化:
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int dp[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
for(int j=m;j>=v;j--){
for(int k=0;k<=s;k++){
if(k*v<=j){
dp[j]=max(dp[j],dp[j-k*v]+k*w);
}
}
}
}
cout<<dp[m]<<endl;
return 0;
}
注意:这里仍然是从大到小枚举j,如果换个顺序的话,那么在算 dp[j]之前,dp[j-k*v]就已经被更新了,这就与原方程不符了
多重背包问题(优化)
经过优化,我们可以解决数据范围更大的多重背包问题
优化策略: 我们可以试图将多重背包问题转化为01背包问题,那么就需要对原本有s个的物品进行拆分,拆分后的数应该能表示0到s的所有情况,我们这里的策略是按s的二进制进行拆分,比如把7,拆为1,2,4,把3拆成1,1,以此类推,如果无法继续(到边界或者再拆就变成负数了),我们就停止拆分 ,随后按分好的份数,变成新的物品加进新物品组,我们可以通过下面的代码具体来感受一下操作过程,并且看看是如何转换为01背包的
#include<iostream>
#include<vector>
using namespace std;
const int N=2100;
int n,m;
int dp[N];
struct node{
int v,w;
};
int main(){
cin>>n>>m;
vector<node>goods;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
for(int k=1;k<=s;k*=2){
s-=k;
goods.push_back({v*k,w*k});
}
if(s>0)goods.push_back({v*s,w*s});
}
for(auto good:goods){
for(int j=m;j>=good.v;j--){
dp[j]=max(dp[j],dp[j-good.v]+good.w);
}
}
cout<<dp[m]<<endl;
return 0;
}
总结: 01背包问题的选或者不选也就与二进制的01对应,我们这样进行这样的拆分,还是拿7举例子,相当于用3个数的选或者不选表示了0到7所有的情况,降低了时间复杂度
分组背包问题
特点: 给N组物品,容量为V的背包,选的时候,一组里面的物品,你最多选一个,仍然求最大价值
状态表示: 用f[i] [j]表示前i组物品,总容量为j的情况下的最大价值
状态转移方程: 暴力思路,可以写出如下的转移方程:
代码实现:
#include<iostream>
#include<vector>
using namespace std;
const int N=110;
struct node{
int v,w;
};
int n,m;
vector<node>g[N];
int dp[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int s,v,w;
cin>>s;
while(s--){
cin>>v>>w;
g[i].push_back({v,w});
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<g[i].size();k++){
if(g[i][k].v<=j){
dp[j]=max(dp[j],dp[j-g[i][k].v]+g[i][k].w);
}
}
}
}
cout<<dp[m]<<endl;
return 0;
}