[题解] 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;
posted @ 2021-08-12 17:28  ¶凉笙  阅读(61)  评论(0编辑  收藏  举报