暑集 Day28 dp专题比赛题解
2023-08-22 10:55:44
前言
注:这场比赛全是 dp 题,时长 3 小时。
第一眼看到 A 题这种奇葩题面直接跳了,感觉不是自己这种成分能写的,结果我旁边两个呆呆鸟 zyx,cf 居然觉得我直接上来把 A 秒了,然后他们死磕 A 题嗑了两个多小时???离谱。
最后我只 A 了三道,拿了最后一题的暴力 30 分,本来以为可能会被卡的结果告诉我给的大样例就是答案???然后 yqt 这个非常邪恶的人直接输出测试数据卡了 100 多分,真的狗啊!!!墙裂谴责!!!!之后蔡lb 觉得懒得改数据(他没有这么说)就说当是给我们信心赛的福利,拿了的都是好事,离大谱。
这次 9 个人 AK,说明确实不难,也说明我实在是太菜了。。。。。

A

\(n\le 1000,a_i\le 1000\)
没看懂,看了题解也不是很清楚。看了 std 后也不懂有了一些新的理解。
原来是这样!就是排序去重以后从后往前 dp,每次取之前取的对于现在的最优解,然后最后看先手能转移到哪个最优解即可(看代码就懂了)。
bool vis[1005];
int n,m,a[1005],f[2][1005];
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++){
a[i]=read();
if(vis[a[i]])n--,i--;
vis[a[i]]=1;
}
sort(a+1,a+1+n);a[n+1]=10000;
for(int i=n;i;--i){
if(a[i+1]-a[i]>m){f[0][i]=a[i],f[1][i]=-a[i];}
else{
f[0][i]=1e9;
for(int j=i+1;j<=n&&a[j]-a[i]<=m;j++)f[0][i]=min(f[0][i],f[1][j]+a[i]);
f[1][i]=-1e9;
for(int j=i+1;j<=n&&a[j]-a[i]<=m;j++)f[1][i]=max(f[0][j]-a[i],f[1][i]);
}
}
int ans=-1e9;
for(int i=1;i<=n&&a[i]<=m;i++)ans=max(ans,f[0][i]);
cout<<ans;
}
B

这就直接打个一眼 dp 就好了,令 \(f_{i,j}\) 表示第 \(i\) 个为 \(j\) 的方案数,转移:
复杂度应该是 \(O(nm\sqrt m)\),\(n,m\le 2000\),可过。
\(Code\)
const int mod=1e9+7;
int n,m;
ll f[2002][2002];
int main(){
cin>>m>>n;
f[0][1]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=j;k<=m;k+=j){
(f[i][k]+=f[i-1][j])%=mod;
}
}
}
ll ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
(ans+=f[i][j])%=mod;
}
}
cout<<ans;
}
C

一眼数位 dp,做就完了。
\(Code\)
int n,mod;
ll f[2][2][2][2][2][2][2][7][1001];
ll dfs(int len,int mu,bool x0,bool x1,bool x2,bool x3,bool x4,bool x5,bool x6){
if(!len)return (x0&x1&x2&x3&x4&x5&x6);
if(~f[x0][x1][x2][x3][x4][x5][x6][mu][len])return f[x0][x1][x2][x3][x4][x5][x6][mu][len];
ll res=0;
for(int i=0;i<=9;i++){
int o=(mu*10+i)%7;
res+=dfs(len-1,o,x0|(o==0),x1|(o==1),x2|(o==2),x3|(o==3),x4|(o==4),x5|(o==5),x6|(o==6));
res%=mod;
}
return f[x0][x1][x2][x3][x4][x5][x6][mu][len]=res;
}
int main(){
memset(f,-1,sizeof(f));
cin>>n>>mod;
printf("%lld",dfs(n,0,0,0,0,0,0,0,0));
}
D
看见这个 \(k\le 10\),果断状压 dp,发现 \(n\) 不大,也不需要矩阵加速(如果矩阵加速状态数也太大了)。
那么我们用到和上次那道模拟赛的状压差不多的方法,我们考虑一个 \(k\) 位二进制数,那么第 \(k-1\) 位表示我当前这位有没有取,第 \(0\) 位表示我前面第 \(k-1\) 个有没有取(当然你也可以通过把第 \(0\) 位变当前位转移),然后令 \(f_{i,j}\) 表示扫到第 \(i\) 个,状态为 \(j\) 的答案,令 \(g_{i,j}\) 表示扫到第 \(i\) 个,状态为 \(j\) 的方案数,\(count_j\) 表示状态 \(j\) 中被选的数的个数,那么转移很好想到,为:
前两个式子表示第 \(i\) 位选的转移,后两个式子表示第 \(i\) 位不选的转移。
然后再预处理一下前 \(k\) 位的答案即可,最后答案就为 \(\sum\limits_{j}^{count_j\in [L,R]}f_{n,j}\)。
对于 \(k>n\) 的情况,我们发现做个组合数就行了,数据范围小,乱搞即可。
\(Origin\ Code\)
const int mod=1e9+7;
int n,k,L,R;
ll a[1010];
ll f[1010][1502],g[1010][1502];
inline bool check(int w){//判断是否合法
int len=__builtin_popcount(w);
if(len<L||len>R)return 0;
return 1;
}
ll C(ll x,ll y){//暴力组合数
ll res=1;
for(ll i=1;i<=x;i++){
res*=i;
}
for(int i=1;i<=y;i++)res/=i;
for(int i=1;i<=x-y;i++)res/=i;
return res;
}
int main(){
n=read(),k=read(),L=read(),R=read();
for(int i=1;i<=n;i++){
a[i]=read();
}
if(k>n){//特判
ll ans=0,w=0;
for(int i=1;i<=n;i++){
w+=C(n-1,i-1);
}
for(int i=1;i<=n;i++){
ans=(ans+a[i]*w%mod)%mod;
}
cout<<ans;
return 0;
}
for(int w=1;w<=k;w++){
for(int x=0;x<(1<<k);x++){
if(x&((1<<k-w+1)-1))continue;//如果发现第 1 位之前都有答案那就不能统计
(f[w][x>>1]+=f[w-1][x])%=mod;
(f[w][(x>>1)|(1<<k-1)]+=(f[w-1][x]+a[w])%mod)%=mod;
}
}
for(int w=k+1;w<=n;w++){
for(int x=0;x<(1<<k);x++){
if(!check(x))continue;//转移过来的状态要合法
if(check(x>>1)){//转移之后的状态也要合法
(f[w][x>>1]+=f[w-1][x])%=mod;
(g[w][x>>1]+=max(1ll,g[w-1][x]))%=mod;
}
if(check((x>>1)|(1<<k-1))){
(f[w][(x>>1)|(1<<k-1)]+=f[w-1][x]+a[w]*max(g[w-1][x],1ll)%mod)%=mod;
(g[w][(x>>1)|(1<<k-1)]+=max(g[w-1][x],1ll))%=mod;
}
}
}
ll ans=0;
for(int i=0;i<(1<<k);i++){
if(!check(i))continue;
ans+=f[n][i];//统计答案
ans%=mod;
}
cout<<ans<<endl;
}
然而这道题调了我巨久,因为我一直都不知道为什么样例二错了,直到看不出代码(摆烂了一个多小时)有什么错误去一个一个输出并模拟样例才发现,原来第 \(i\) 位的贡献不是直接加的,要乘上对应转移式的方案数,然后又调来调去调了很久,因为一开始以为方案数是什么二的平方然后写了个快速幂,又觉得快速幂的指数要取模 \(mod-1\) 什么的,对后面的调试犯下了很多错,最后这道题花了我近两个小时调完,没时间思考第一题和最后一题了(二三题太水了加起来不到半个小时秒了)。
还是感觉我这个代码写的太冗长了,看了一下 std,觉得有一些值得修改的地方(比如为 \(k>n\) 专门写了个组合数???)。
\(Refactored\ Code\)
int n,k,L,R;
const int mod=1e9+7;
ll a[1010],f[1010][1502],g[1010][1502],ans;
inline bool check(int w){
int len=__builtin_popcount(w);
return (len>=L&&len<=R);
}
ll cal(int x){
ll res=0,cnt=0;
for(;x;x>>=1)res=(res+(x&1)*a[++cnt])%mod;
return res;
}
int main(){
n=read(),k=read(),L=read(),R=read();
for(int i=1;i<=n;i++)
a[i]=read();
if(k>n)
k=n,L=1,R=n;
for(int x=0;x<(1<<k);x++){
if(!check(x))continue;
f[k][x]=cal(x);
g[k][x]=1;
}
for(int w=k+1;w<=n;w++){
for(int x=0;x<(1<<k);x++){
if(!check(x))continue;
if(check(x>>1)){
(f[w][x>>1]+=f[w-1][x])%=mod;
(g[w][x>>1]+=g[w-1][x])%=mod;
}
if(check((x>>1)|(1<<k-1))){
(f[w][(x>>1)|(1<<k-1)]+=f[w-1][x]+a[w]*g[w-1][x]%mod)%=mod;
(g[w][(x>>1)|(1<<k-1)]+=g[w-1][x])%=mod;
}
}
}
for(int i=0;i<(1<<k);i++){
ans=(ans+f[n][i])%mod;
}
printf("%lld\n",ans);
}
好看多了。
E

\(n,m\le 5000,0\le a_i\le 1000\)
先不慌不忙地打个深搜拿 30 分,然后发现答案似乎比方案好求,那是不是要用某种方法先算出答案,然后再根据答案去算方案啊。来不及想了,打完暴力就跑去写第一题了。赛后发现真的是先二分计算答案,然后再算答案 \(\le ans\) 的方案数,显然答案已经最小了,所以不会有小于的,那么也就是正确的方案数了。
我们可以令 \(f_{i,k}\) 表示当前第 \(i\) 位看完休息后,一共休息了 \(k\) 次的方案数。那么:
\(j\) 满足 \(\sum_{x=j+1}^i a_x\le ans\)。
维护个前缀和就好了,注意到 \(n\) 之后不能休息,不过我们可以强制它休息,给 \(m\) 加个 \(1\) 就行。
复杂度是 \(O(n^2m)\) 的,不过因为数据太水,居然也直接碾过去了,而且还跑飞快。
\(Code1\)
const int mod=1e9+7;
int n,m,a[5002],ans,pre[5002];
ll f[5002][5002];//200MB
bool check(int x){
int t=1,res=0;
for(int i=1;i<=n;i++){
if(a[i]+res<=x)res+=a[i];
else if(a[i]<=x)res=a[i],t++;
else return 0;
if(t>m)return 0;
}
return 1;
}
int cal(){
int l=1,r=pre[n];
while(l<=r){
int mid=l+r>>1;
if(check(mid))r=mid-1;
else l=mid+1;
}
return l;
}
int main(){
n=read(),m=read()+1;
for(int i=1;i<=n;i++)
pre[i]=pre[i-1]+(a[i]=read());
ans=cal();
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
if(pre[i]-pre[j]<=ans)
for(int k=1;k<=m;k++)
f[i][k]=(f[i][k]+f[j][k-1])%mod;
ll sum=0;
for(int i=1;i<=m;i++)sum=(sum+f[n][i])%mod;
printf("%d %lld\n",ans,sum);
}
瓶颈在枚举 \(j\),我们发现 \(j\) 的指针只会增大不会减小,而且对答案的贡献是一个区间,所以我们只需要维护对于每个 \(i\) 最小满足条件的位置 \(l\) 即可。那么这个区间的答案因为都能算到贡献里,所以我们用一个 \(sum_{i,j}\) 维护方案前缀和 \(\sum_{x=1}^i f_{x,j}\) 即可,那么对区间 \([l,i]\) 的贡献即为 \(sum_{i,j}-sum_{l-1,j}\)。
\(Code2\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
int read(){
int x=0,f=1;char c=getchar();
for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
int n,m,a[5002],ans,pre[5002];
ll f[5002][5002],sum[5002][5002];//400MB
bool check(int x){
int t=0,res=0;//m这次不变了,因为我们是从下标[1,0]开始计算,那么还是相当于转移式变成了 i 之前休息了几次。
for(int i=1;i<=n;i++){
if(a[i]+res<=x)res+=a[i];
else if(a[i]<=x)res=a[i],t++;
else return 0;
if(t>m)return 0;
}
return 1;
}
int cal(){...}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)
pre[i]=pre[i-1]+(a[i]=read());
ans=cal();
int l=0;
for(int i=1;i<=n;i++){
if(pre[i]<=ans)f[i][0]=1;//i之前休息了0次
sum[i][0]=(sum[i-1][0]+f[i][0])%mod;//0次的贡献
while(pre[i]-pre[l]>ans)l++;
for(int j=1;j<=m;j++){//i 之前休息了 j 次
f[i][j]=((sum[i-1][j-1]-(l>0?sum[l-1][j-1]:0))%mod+mod)%mod;//这里注意[i,j]是从[[l,i-1],j-1]转移的,所以因为是前缀和,l 需要减一
sum[i][j]=(sum[i-1][j]+f[i][j])%mod;
}
}
ll w=0;
for(int i=1;i<=m;i++)
w=(w+f[n][i])%mod;
printf("%d %lld\n",ans,w);
}
题解看了好久,最后发现它那个码风很怪导致我以为它是从 下标0开始转移的,然后导致我一开始一直 WA,后面自己思考了才知道是从下标1开始,转移式也跟我的不太一样,而且我这个地方写的是之前休息的次数了。

浙公网安备 33010602011771号