20260518 - 背包 总结
注意到 J 是 *2100 的题,什么叫我们班人均 *2100 /jk /jk /jk /dk /dk /dk 太可怕了!
我居然在人均题吃了三发罚时才过,喜提我所有题中吃的最多的罚时。
没救了没救了。
背包
背包,也叫背包 DP(骗你的其实背包才是简称),是一种 DP 的类型,通常形式为在代价限制下选择价值尽可能高的物品。
背包又分多种类型,如 \(0/1\) 背包、完全背包、多重背包、混合背包,且背包有各种不同的运用。
\(0/1\) 背包
\(0/1\) 背包,顾名思义就是一个物品只有 \(0/1\) 两种选择——即选与不选。在 \(0/1\) 背包中,每种物品的数量都是 \(1\)。
\(0/1\) 背包最常见的状态设计为:当前考虑了前 \(i\) 个物品且代价总和为 \(j\) 的情况下最终能达到的最高总价值。当对于一个代价为 \(w_i\)、价值为 \(v_i\) 的物品进行转移时,转移方程为 \(dp_{i,j} = \max(dp_{i,j} , dp_{i-1,j-w_i}+v_i)\)。
完全背包
完全背包最核心的特征就是每种物品的数量都是 \(\infty\),这意味着一件物品可以选任意多次——和 \(0/1\) 背包完全是反过来的。
完全背包的状态设计与 \(0/1\) 背包相似,但一个不同的点在于:我们做完全背包的时候,经常忽略 DP 状态设计中的第一个维度 \(i\)——这里可不单单是节省了空间,因为你在忽略 \(i\) 时,直接顺序枚举 \(j\) 后转移 \(dp_j = \max(dp_j , dp_{j-w_i}+v_i)\) 就可以直接实现完全背包的效果了。
为什么呢?你可以看看下面【压维(滚动数组)】部分呀。
因为此时 \(j\) 是从小到大枚举的,那么走到 \(dp_j\) 时,转移方程涉及到的 \(dp_{j-w_i}\) 可能已经被修改过了,此时再转移就用了多次 \(j\) 物品,也就完美实现了我们“一种物品可以选任意次”的效果。
多重背包
多重背包,我个人觉得它是一种介于 \(0/1\) 背包和完全背包之间的定义(毕竟其实现方式还是难于前两种的)。猜都能猜到,多重背包就是每种物品的数量为一个指定值 \(c_i\),一类物品本质是一样的,数量虽不为 \(1\) 但也有限。
此时 \(0/1\) 背包和完全背包就都在这里派不上用场了,但我们可以发现,如果一种物品有 \(c_i\) 个,那我们就把这个物品的单个形式复制 \(c_i\) 份,全部存起来,不就变成 \(0/1\) 背包了吗?这么做肯定没错,但是如果 \(\sum c_i\) 太大了存不下呢?
此时就要用到一个很简单的知识,我们发现,任意一个 \(< 2^k\) 的数,都可以被 \(1,2,4,\dots,2^{k-1}\) 这些数中的若干个的和凑出来,换算成二进制是很容易理解的。把这个思想运用到拆物品上面来,我们对于一个 \(c_i\),也将其拆成若干个连续 \(2\) 的幂次不就行啦。
当然不行,谁告诉你 \(c_i\) 恰好就是 \(2^k-1\) 的?那怎么办呢?没事,我们找到最大但不超过 \(c_i\) 的 \(2^k-1\),把剩下部分 \(c_i-(2^k-1)\) 再额外开一个即可。
如何证明这个东西是对的呢,很简单,首先 \(2^k-1\) 以内的都可以通过最开始的一串 \(1,2,4,\dots,2^{k-1}\) 凑出来;而大于 \(2^k-1\) 的部分,可以先选一个 \(c_i-(2^k-1)\),那么剩下的就又进到 \(2^k-1\) 以内了,再用之前的凑就行了。
这样我们的数量就是 \(O(n \log n)\) 的量级了,然后再做 \(0/1\) 背包就解决了。
混合背包
混合背包属于最复杂的一种情况(吗?),顾名思义就是混合的背包,也就是前三种背包都有——给定若干个物品,有的物品只有一个(\(0/1\) 背包),有的物品有 \(\infty\) 个(完全背包),还有的物品有指定数量个(多重背包)。
啊???单个的我都会做啊,混在一起怎么搞?
注意到我们之前做多重背包的时候,是用一些巧妙的二进制方法把它变回了 \(0/1\) 背包,那是不是可以考虑把完全背包也变成 \(0/1\) 背包呢?
对!虽然完全背包说是能选任意个,但我们背包总是有限制的呀,你不可能真的选上个 \(\infty\) 个,所以我们只需要根据代价限制和物品代价做商就能算出所谓的 \(\infty\) 个的真正上限是多少了。然后就也变成多重背包了,不就做完了嘛。
压维(滚动数组)
某些时候,我们在做背包,其时间复杂度是没问题的,但是开那么大的数组空间会爆炸,怎么办?难道不做了嘛?
不可能啊,所以这里介绍一种最常见也最好用的省空间方案——压维(即滚动数组)。
一般情况下,做背包时,转移第 \(i\) 层只需用到第 \(i-1\) 层的信息,那么我们是不是可以不用维护总共的 \(n\) 层,只维护当前层和上一层呢?当然可以呀,这样不就能把第一维原来的 \(n\), 直接优化为常数 \(2\),很强吧!
但这不是最优的。我们可以考虑把这个 \(2\) 的常数干掉,怎么干掉呢?当然就是只用一个维度存了。但是我们需要确保用到的都是上一层的东西,因此,如果你的转移方程牵连到的是 \(<j\) 的位置,那么我们就需要在遍历的时候倒序枚举 \(j\),这样就能确保我们转移的时候用到的都是第 \(i-1\) 层而不是第 \(i\) 层已经更新过的东西了。
不能用到自己层的东西,唯一就是防止了一个物品被选多次,因此这就和前面完全背包的部分呼应了——我不是特意强调了完全背包要顺序枚举 \(j\) 吗,就是这个原因,因为完全背包就是希望一个物品被选多次嘛!
例题选讲
实则全讲。嘟嘟嘟。
会附上算法标签、罚时原因(没有罚时就没有这个环节 qwq)和时间复杂度分析。嘟嘟嘟。
A - 采药
算法标签:\(0/1\) 背包。
罚时原因:
- DP 的时候没有继承上一次的答案。
你们不觉得在第一题就吃罚时很招笑吗。
我们定义 \(dp_{i,j}\) 表示当前考虑了前 \(i\) 株草药且目前总花费时间为 \(j\) 的情况下能采到的草药价值最大和。
转移时,我们直接顺序枚举 \(i\) 和 \(j\),先继承 \(dp_{i,j} = dp_{i-1,j}\)(也就是不采第 \(i\) 株草药的情况),随后判断只要 \(j \ge t_i\)(也就是时间足够采下第 \(i\) 株草药),那么转移 \(dp_{i,j} = \max(dp_{i,j} , dp_{i-1,j-t_i} + v_i)\) 即可。
最终答案即为 \(dp_{n,m}\)。
时间复杂度:\(O(nm)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 105;
int n,m,v[N],t[N],dp[N][1005],Ans;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
m=read(),n=read();
for(int i=1;i<=n;i++)
t[i]=read(),v[i]=read();
for(int i=1;i<=n;i++)
for(int s=0;s<=m;s++){
dp[i][s]=dp[i-1][s];
if(s>=t[i])dp[i][s]=max(dp[i][s],dp[i-1][s-t[i]]+v[i]);
}
cout<<dp[n][m]<<"\n";
return 0;
}
B - 疯狂的采药
算法标签:完全背包。
题目都说了是一株草药可以无限制疯狂采摘,那不就是完全背包的板题吗?
定义 \(dp_j\) 表示当前考虑了前 \(i\) 株草药且目前总花费时间为 \(j\) 的情况下能采到的草药价值最大和。
顺序枚举 \(i\),也顺序枚举 \(j\)(需确保 \(j \ge t_i\),不然你就无法更新 DP 状态),转移让 \(dp_j = \max(dp_j , dp_{j-t_i} + v_i)\) 即可。最终答案 \(dp_m\)。
时间复杂度:\(O(nm)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int T = 1e7+5;
const int N = 1e4+5;
LL n,m,t[N],v[N],dp[T];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
m=read(),n=read();
for(int i=1;i<=n;i++)
t[i]=read(),v[i]=read();
for(int i=1;i<=n;i++)
for(int j=t[i];j<=m;j++)
dp[j]=max(dp[j],dp[j-t[i]]+v[i]);
cout<<dp[m]<<"\n";
return 0;
}
C - Cut Ribbon
算法标签:完全背包。
你考虑定义 \(dp_i\) 表示长度恰好为 \(i\) 时用到的最多的丝带段数,最初 \(dp_0 = 0\) 但其他 \(i\) 有 \(dp_i = - \infty\)。
转移时,我们先顺序枚举 \(i\),然后分类:
- 若 \(i \ge a\):则这个位置可能是以一段长度为 \(a\) 的丝带为结尾的,于是转移 \(dp_i = \max(dp_i , dp_{i-a} + 1)\)。
- 若 \(i \ge b\):同理 \(dp_i = \max(dp_i , dp_{i-b} + 1)\)。
- 若 \(i \ge c\):同理 \(dp_i = \max(dp_i , dp_{i-c} + 1)\)。
最后的答案就是 \(dp_n\)。
时间复杂度:\(O(n)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 4005;
LL n,a,b,c,dp[N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),a=read(),b=read(),c=read();
memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++){
if(i>=a)dp[i]=max(dp[i],dp[i-a]+1);
if(i>=b)dp[i]=max(dp[i],dp[i-b]+1);
if(i>=c)dp[i]=max(dp[i],dp[i-c]+1);
}
cout<<dp[n]<<"\n";
return 0;
}
D - Knapsack 2
算法标签:\(0/1\) 背包。
乍一看是一道简单的 \(0/1\) 背包,但定睛一看会发现 \(W \le 10^9\)!显然,时间会爆空间也会爆,怎么办呢?
别急,我们注意到 \(v_i \le 10^3\),那既然代价很大而价值很小,我们就反过来,让价值定义进状态,让代价存进 DP 值不就行了嘛。
具体地,我们定义 \(dp_{i,j}\) 表示当前考虑了前 \(i\) 件物品且当前价值总和为 \(j\) 的情况下的最小代价和。
转移方程是 \(dp_{i,j} = \min(dp_{i,j} , dp_{i-1,j-v_i} + w_i)\)。但是要注意的点是,此时的 \(\sum v_i\) 有 \(10^5\) 级别,开二维数组非常勉强,因此我们可以用压维的方式让 DP 数组只剩下一维。
压维后,转移的时候就一定要记得 \(j\) 是倒序枚举了。
最后只需要倒序枚举找到最大的 \(dp_i \le W\) 的 \(i\) 输出即可。
时间复杂度:\(O(n \sum v_i)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 105;
const int M = 1e5+5;
LL n,W,w[N],v[N],dp[M],sum,Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),W=read();
for(int i=1;i<=n;i++)
w[i]=read(),v[i]=read(),sum+=v[i];
memset(dp,0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
for(int j=sum;j>=v[i];j--)
dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
for(int i=sum;i>=0;i--)
if(dp[i]<=W){Ans=i;break;}
cout<<Ans<<"\n";
return 0;
}
E - 宝物筛选
算法标签:多重背包。
显然就是一个多重背包的板子。
就像前面说的,我们先把物品个数以拆二进制的形式,化成 \(O(n \log n)\) 个单个物品,然后再做板子 \(0/1\) 背包即可。
哦以及这题的范围也比较大,最好压维优化一下。我再说最后一遍压维了一定要倒序枚举 \(j\)!
时间复杂度:\(O(n \log{n} \times W)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
LL n,W,cnt,v[N],w[N],dp[N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),W=read();
for(int i=1;i<=n;i++){
LL x=read(),y=read(),c=read();
LL now=1;
while(c>=now)
++cnt,v[cnt]=x*now,w[cnt]=y*now,
c-=now,now*=2ll;
if(c)++cnt,v[cnt]=x*c,w[cnt]=y*c;
}
for(int i=1;i<=cnt;i++)
for(int j=W;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[W]<<"\n";
return 0;
}
F - Checkout Assistant
算法标签:背包。
这个真不好归类说它是 \(0/1\) 背包或者完全背包或者多重背包或者混合背包什么的吧。T^T
对于一个物品,我们有两种选择:第一种,就是为这个物品买单,然后多出来的 \(t_i\) 时间可用于取走其他物品;第二种,就是考虑偷走这个物品,这需要消耗前面预留出来的 \(1\) 单位时间。
这样就可以做背包 DP 了,我们定义 \(dp_{i,j}\) 表示当前考虑了前 \(i\) 个物品且当前预留的总时间为 \(j\) 的情况下最少需要花费多少元,转移方程 \(dp_{i,j+t_i} = dp_{i-1,j} + c_i\) 以及 \(dp_{i,j-1} = dp_{i-1,j}\)。
哦,以及预留的总时间达到 \(n\) 后再多预留多少效果都没区别了,因此其实不需要维护那么多,如果转移的时候状态超过 \(n\) 了就和 \(n\) 取个 \(\min\)。
最终答案就是 \(\min_{0 \le i \le n} dp_{n,i}\)。
还有一个就是,转移的过程中预留总时间可能变成负数诶,怎么办——?没事我们 \(+n\) 偏移一下就行啦。
时间复杂度:\(O(n^2)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2005;
struct node{LL t,c;}a[N];
LL n,dp[N][N*2],Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i].t=read(),a[i].c=read();
memset(dp,0x3f,sizeof(dp));
dp[0][n]=0;
for(int i=1;i<=n;i++){
for(int x=0;x<=2*n;x++){
int y=min(x+a[i].t,2*n);
dp[i][y]=min(dp[i][y],dp[i-1][x]+a[i].c);
if(x<2*n)dp[i][x]=min(dp[i][x],dp[i-1][x+1]);
}
}
Ans=0x3f3f3f3f3f3f3f3f;
for(int x=n;x<=n+n;x++)Ans=min(Ans,dp[n][x]);
cout<<Ans<<"\n";
return 0;
}
G - The Values You Can Make
算法标签:\(0/1\) 背包。
为什么这题过的人会这么少???明明很简单呐!
观察到 \(n,k\) 什么的都很小只有 \(500\),不妨暴力一点,设 \(dp_{i,x,y}\) 表示当前考虑了前 \(i\) 个数、总和为 \(x\)、子集总和为 \(y\) 的情况是否存在。也就是说,这是一个 bool 类型的 DP 数组。
我们最终要统计的,就是满足 \(dp_{n,k,u} = \text{true}\) 的 \(u\)。
那么,怎么转移呢?先枚举 \(i\),然后枚举上一次的 \(x\) 和 \(y\)(注意 \(y \le x\))。我这里采用的是扩散型转移。显然可以先继承上一次的答案(不选 \(i\) 这个值)。接着就是选 \(i\) 了,\(x\) 一定要加的,但 \(y\) 有选与不选两种方案(\(0/1\) 背包嘛),分别都转移一下即可:
dp[i][x+c[i]][y+c[i]]|=dp[i-1][x][y];
dp[i][x+c[i]][y]|=dp[i-1][x][y];
由于是 bool 类型的 DP,所以还可以用 bitset 优化一下让它跑得更快,但其实没必要啊。我没这么做。
时间复杂度:\(O(n \times k^2)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 505;
LL n,k,a[N],cnt,q[N];
bool dp[N][N],f[N][N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void copy_(){
for(int i=0;i<=k;i++)
for(int j=0;j<=k;j++)f[i][j]=dp[i][j];
return;
}
int main(){
n=read(),k=read();
for(int i=1;i<=n;i++)a[i]=read();
dp[0][0]=1;
for(int i=1;i<=n;i++){
copy_();
for(int x=0;x+a[i]<=k;x++)
for(int y=0;y<=x;y++)if(dp[x][y])
dp[x+a[i]][y]|=f[x][y],
dp[x+a[i]][y+a[i]]|=f[x][y];
}
for(int i=0;i<=k;i++)
if(dp[k][i])q[++cnt]=i;
cout<<cnt<<"\n";
for(int i=1;i<=cnt;i++)
cout<<q[i]<<" ";cout<<"\n";
return 0;
}
H - Bottles
算法标签:背包。
罚时原因:
- 在没必要开
long long的时候开long long导致 TLE(本来就卡在时限边缘)。
显然对于一个瓶子只有两种方案——自己留下来当瓶子,或全部倒进别人。可以很容易地证明留一部分这种情况一定是不优的。
那么就好做了,定义 \(dp_{i,j,x}\) 表示当前考虑了前 \(i\) 个瓶子、有 \(j\) 个瓶子选择保留下来且当前剩余容量为 \(x\) 的情况下最少的要倒的量(按照题目的定义,这个就是要花费的时间,也就是答案)。
怎么转移呢?我们枚举 \(i,j,x\),像最开始说的两种方案分别转移即可(我用的扩散型)。自己留下来当瓶子 \(dp_{i,j+1,x+b_i-a_i} = dp_{i-1,j,x}\),全部倒进别人 \(dp_{i,j,x-a_i} = dp_{i-1,j,x} + a_i\)。
这样就做完了!最后通过枚举找到最小的 \(j\),再求出 \(\min_{0 \le x \le \sum b_i} dp_{n,j,x}\) 即可。
时间复杂度:\(O(n^2 \sum b_i)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 105;
const int NN = 2e4+5;
const int INF = 0x3f3f3f3f;
int n,sum,a[N],b[N];
int dp[N][NN],f[N][NN];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void copy_(){
for(int i=0;i<=n;i++)
for(int j=0;j<=2*sum;j++)
f[i][j]=dp[i][j],dp[i][j]=INF;
return;
}
void upd(int &x,int y){x=min(x,y);return;}
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)b[i]=read(),sum+=b[i];
memset(dp,0x3f,sizeof(dp));
dp[0][sum]=0;
for(int i=1;i<=n;i++){
copy_();
for(int j=0;j<=n;j++)
for(int x=0;x<=2*sum;x++){
if(j<n&&x+b[i]-a[i]<=2*sum)
upd(dp[j+1][x+b[i]-a[i]],f[j][x]);
if(x>=a[i])upd(dp[j][x-a[i]],f[j][x]+a[i]);
}
}
for(int i=0;i<=n;i++){
int min_=INF;
for(int x=sum;x<=2*sum;x++)upd(min_,dp[i][x]);
if(min_<INF){cout<<i<<" "<<min_<<"\n";break;}
}
return 0;
}
I - Fire
算法标签:\(0/1\) 背包。
罚时原因:
- 没对物品排序导致方案不一定最优。
就像上面罚时原因说的——显然不能直接按照题目输入的顺序开始做 DP 嘛,因为题目的“拯救”是有先后顺序的,顺序不同结果就截然不同,所以需要以一种最优的顺序出发转移。
\(d_i\) 越小,第 \(i\) 个物品就死的越早(废话),对于一个物品序列,我们按照 \(d_i\) 从小到大取一定不会劣于其他方案,因此我们可以对 \(d_i\) 升序排序,这样就可以正常做背包了。
那么,就是非常简单的操作了。我们定义 \(dp_{i,j}\) 表示当前考虑了前 \(i\) 个物品、当前时间为 \(j\) 的情况下能拯救的最大物品价值之和。
枚举 \(i,j\),不救物品 \(i\) 的情况也就是继承。考虑救物品 \(i\),如果 \(j+t_i < d_i\),则从时刻 \(j\) 开始可以救物品 \(i\)(对还是扩散型),于是就有转移 \(dp_{i,j+t_i} = dp_{i-1,j} + v_i\)。
最后的答案就是 \(\max_{0 \le t \le \max d_i} dp_{n,t}\)。
哦至于方案,DP 的时候不要直接取 \(\max\),如果更新答案了就记录一下是从哪个位置转移过来的,最后倒序从 \(n\) 出发还原一下即可。
时间复杂度:\(O(n \sum d_i)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 105;
const int T = 2005;
struct node{int t,d,v,id;}a[N];
int n,mxd;
int dp[N][T],ths[N][T],lst[N][T],tim;
stack<int> st;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(node d1,node d2){return d1.d<d2.d;}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i].t=read(),a[i].d=read(),
a[i].v=read(),a[i].id=i,mxd=max(mxd,a[i].d);
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
for(int s=0;s<=mxd;s++)
dp[i][s]=dp[i-1][s],
ths[i][s]=ths[i-1][s],
lst[i][s]=lst[i-1][s];
for(int s=a[i].d-a[i].t-1;s>=0;s--){
int nxval=dp[i-1][s]+a[i].v;
if(nxval>dp[i][s+a[i].t])
dp[i][s+a[i].t]=nxval,
ths[i][s+a[i].t]=i,
lst[i][s+a[i].t]=ths[i-1][s];
}
}
for(int s=0;s<=mxd;s++)
if(dp[n][s]>dp[n][tim])tim=s;
cout<<dp[n][tim]<<"\n";
int u=n,now=tim;
while(u){
if(ths[u][now])st.push(ths[u][now]);
int nnow=now-a[u].t,nu=lst[u][now];
now=nnow,u=nu;
}
cout<<st.size()<<"\n";
while(!st.empty()){cout<<a[st.top()].id<<" ";st.pop();}
cout<<"\n";
return 0;
}
J - Round Subset
算法标签:\(0/1\) 背包。
罚时原因:
- 用类型为
int的read函数读取了long long数值。- 数组维度开反(用反)。
- 压维没有两个维度都倒序枚举。
感觉都是一些很智障的问题啊,怎么回事呢。
\(10 = 2 \times 5\) 这个是大家都知道的,所以末尾 \(0\) 的情况就取决于这些数所含的 \(2\) 和 \(5\) 的个数的和的 \(\min\) 值。
那么其实每个 \(a_i\) 的具体值我们是不关心的,只需要求出 \(s2\) 和 \(s5\) 表示每个数包含的 \(2\) 和 \(5\) 的个数。这个个数是 \(\log\) 级别的。
考虑到 \(5\) 的个数均摊比 \(2\) 的个数少(因为 \(5>2\) 嘛),我们把 \(5\) 的个数设计进状态,即 \(dp_{i,j,x}\) 表示当前考虑了前 \(i\) 个数、选了 \(j\) 个数且选的这些数的 \(5\) 的个数之和为 \(x\) 的情况下能取到的最大的 \(2\) 的个数之和。
转移是很简单的啦!枚举 \(i,j,x\),先继承 \(dp_{i,j,x} = dp_{i-1,j,x}\) 表示不选 \(i\) 这个数,接着就是转移 \(dp_{i,j+1,x+s5_i} = dp_{i-1,j,x} + s2_i\)(依旧扩散型哈),不就结束了嘛。
最终的答案是 \(\max_{0 \le x \le \sum s5_i} \min(x,dp_{n,k,x})\)。为什么 \(x\) 要和 \(dp_{n,k,x}\) 取 \(\min\) 呢,因为末尾 \(0\) 的构成需要 \(2\) 和 \(5\) 同时出现呀。
数组开不下要滚动数组一下,以及记得枚举的时候两个维度都要倒序啊喂!!!
时间复杂度:\(O(n^3 \times \sum \log_5 a_i)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 205;
const int M = 6005;
int n,k,sum,s5[N],s2[N],dp[N][M],Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),k=read();
for(int i=1;i<=n;i++){
LL num=read();
while(num%2ll==0)num/=2ll,s2[i]++;
while(num%5ll==0)num/=5ll,s5[i]++;
sum+=s5[i];
}
memset(dp,-0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)
for(int x=sum;x>=s5[i];x--)
for(int p=min(k,i);p>=1;p--)
dp[p][x]=max(dp[p][x],dp[p-1][x-s5[i]]+s2[i]);
for(int x=0;x<=sum;x++)
Ans=max(Ans,min(x,dp[k][x]));
cout<<Ans<<"\n";
return 0;
}
K - 选课
算法标签:树上背包,\(0/1\) 背包。
老师加了一个题试图防 AK,可这个题是我【三年前的回忆】(为什么这么说呢你可以翻翻我洛谷这题的所有提交记录),很快就能秒掉啊。。。。
发现这个所谓的依赖其实就是一个树形结构,但是直接建出来是一个森林,不好搞,所以我们建一个 \(0\) 表示一个虚拟的根,让所有“根”都连向这个 \(0\),这样就是一棵正常的树了。
我们定义 \(dp_{u,i}\) 表示当前考虑了 \(u\) 的整个子树且总共选了 \(i\) 门课能获得的最大学分。
从 \(0\) 出发做 DFS,假设当前考虑到了 \(u\),那么最开始有一个初值 \(dp_{u,1} = s_u\) 也就是只选 \(u\) 这一门课。枚举 \(u\) 的每个儿子 \(v\) 先下去 DFS 处理子问题,然后考虑更新 \(u\) 的答案。枚举总共选 \(i\) 个、在 \(v\) 这个子树内选 \(j\) 个(要求 \(j<i\) 因为你不能不选 \(u\) 啊),转移 \(dp_{u,i} = dp_{u,i-j} + dp_{v,j}\) 取最大值即可。
最终的答案是 \(dp_{0,m+1}\),为什么是 \(m+1\) 呢,题目说的可是 \(m\) 门课程呀!但是我们新建了一个虚拟的根 \(0\) 啊,这个 \(0\) 要是不选下面的课就都选不了呢,所以就变成 \(m+1\) 门课了呀。
时间复杂度:\(O(n^3)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 305;
LL n,m,fa[N],dp[N][N];
vector<int> g[N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void DFS_dp(int u){
for(int v:g[u]){
DFS_dp(v);
for(int i=m+1;i>=1;i--)
for(int j=0;j<i;j++)
dp[u][i]=max(dp[u][i],dp[u][i-j]+dp[v][j]);
}
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)
fa[i]=read(),dp[i][1]=read(),g[fa[i]].pb(i);
DFS_dp(0);cout<<dp[0][m+1]<<"\n";
return 0;
}
L - Travelling
老师讲的没太听懂,之后来补。
总结
背包,是 DP 中的一种,其分类广泛(\(0/1\) 背包、完全背包、多重背包、混合背包等等),用途也非常广,是一种很厉害的 DP 喵!
Thanks reading.

浙公网安备 33010602011771号