[题解] P7519 [省选联考 2021 A/B 卷] 滚榜
P7519 [省选联考 2021 A/B 卷] 滚榜
状压 \(DP\) + 费用提前
首先这道题对于 \(n\leq 10\) 的数据,可以枚举全排列 \(O(n*n!)\) 来贪心的分配,看看最后一个能不能符合题意即可。
这样就有 \(60 \ pts\) 的好成绩了
不难想到这是个状压 \(DP\),状态设计为 \(f[s,j,k]\) 表示分配完了集合 \(s\) ,花费代价为 \(j\) ,上一个人的总题目数量为 \(k\) 的方案数,然而这比暴力还慢。
想一下发现我们只关心上一个人是谁,因此没必要把他的题目数作为状态。
- 因此就需要费用提前
由于 \(b\)单调不降并且总成绩有一定的规律,假设当前给 \(i\) 这个队滚了 \(k\) 个题目,那么以后的队一定免不了滚这 \(k\) 个题目
发现了这个性质,可以进行费用提前来通过直接比较 \(a\) 数组优化状态和状态复杂度
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define LL long long
const int maxn = 15,maxm = 505,maxs = 10005;
int n,m;
int a[maxn];
LL ans=0;
LL f[maxs][maxm][maxn];
int mx,maxid;
int main(){
scanf("%d%d",&n,&m);
int S=(1<<n)-1;
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
if(mx<a[i]){
mx=a[i];maxid=i;
}
}
for(int i=0;i<n;i++){
if(i<=maxid)f[1<<i][n*max(0,mx-a[i])][i]=1;
else f[1<<i][n*max(0,mx-a[i]+1)][i]=1;
}
for(int s=0;s<=S;s++){
int sz=0;
for(int i=0;i<n;i++)if((s>>i)&1)sz++;
if(sz==1)continue;//没什么可转移的
for(int i=0;i<n;i++){//枚举当前转移
if(!((s>>i)&1))continue;
for(int la=0;la<i;la++){
if(!((s>>la)&1))continue;
int tmp=max(0,a[la]-a[i]+1)*(n-sz+1);
for(int j=tmp;j<=m;j++)f[s][j][i]+=f[s-(1<<i)][j-tmp][la];
}
for(int la=i+1;la<n;la++){//枚举上一个转移
if(!((s>>la)&1))continue;
int tmp=max(0,a[la]-a[i])*(n-sz+1);
for(int j=tmp;j<=m;j++)f[s][j][i]+=f[s-(1<<i)][j-tmp][la];
}
}
}
for(int i=0;i<=m;i++)
for(int j=0;j<n;j++){
ans+=f[S][i][j];
}
printf("%lld\n",ans);
return 0;
}
后记
- 借这道题来记录一下刷表法的注意事项
首先是刷表法的代码。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define LL long long
const int maxn = 15,maxm = 1000,maxs = 10005;
int n,m;
int a[maxn];
LL ans=0;
LL f[maxs][maxm][maxn];
int mx,maxid;
int main(){
scanf("%d%d",&n,&m);
int S=(1<<n)-1;
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
if(mx<a[i]){
mx=a[i];maxid=i;
}
}
for(int i=0;i<n;i++){
if(i<=maxid)f[1<<i][n*max(0,mx-a[i])][i]=1;
else f[1<<i][n*max(0,mx-a[i]+1)][i]=1;
}
for(int s=0;s<S;s++){
int sz=0;
for(int i=0;i<n;i++)if((s>>i)&1)sz++;
if(sz!=0)
for(int i=0;i<n;i++){//枚举当前转移
if(!((s>>i)&1))continue;
for(int j=0;j<=m;j++)
if(f[s][j][i])
for(int l=0;l<n;l++){
if((s>>l)&1)continue;
int tmp=0;
if(a[l]<=a[i])tmp=a[i]-a[l]+(l>i);
tmp=tmp*(n-sz)+j;
if(tmp>m)continue;
//cerr<<sz<<endl;//
f[s|(1<<l)][tmp][l]+=f[s][j][i];
}
}
}
// for(int i=0;i<=m;i++)
// for(int j=0;j<n;j++)printf("%lld\n",f[S][j][i]);
for(int i=0;i<=m;i++)
for(int j=0;j<n;j++){
ans+=f[S][i][j];
}
printf("%lld\n",ans);
return 0;
}
刷表法有些题是很好用的,但是需要保证每次都 从合法状态开始刷
比如上面,当 \(f[s][j][i]!=0\) 时才可以开始刷,否则,在有些情况是会把表刷大的。(测试这题不会)
数组越界有可能会 \(WA\) ,因此尽量排除不合法选项:
if(tmp>m)continue;