[题解] P5888 传球游戏

一道非常好的 dp 和理解滚动数组的题

题目传送门

理解题意(做题即翻译)

有 n 个人摆成环形在玩传球游戏,每个人可以传给任意编号的人,但是某个 zz 觉得太简单了,于是加了 k 条限制:其格式为 u 不可传球到 v 这个人,问 m 次传球后回到 1 的方案数是多少。

数据范围很重要

n <= 1e9 , k <= 5e4。

睁眼一看,我靠n怎么这么大,这要O(n)???!!!

状态选择

明显地,最好选择传了 i 次球为状态,考虑求在第 j 个人的手里的方案数是 \(f[ i ][ j ]\),结果便是 \(f[ m ][ 1 ]\)

然而这会爆空间,因此本题的亮点之一滚动数组就派上用场了,我们在状态转移的时候再谈。

状态转移

我们发现 n 很大,但是 k 却不是很大,回到题意,也就是说受限制的人很少,设受限制的人(称为特殊人)为 k 个(包括1号),则对于其余 n - k 个人都是一样的,所以我们把 0 号看成一般人,1-k 号看成特殊人进行状态转移。

转移方法:

思路有了,方法也就差不多出来了。

$f[ i ][ j ] = sigma(f[ i - 1 ][ j ](j != 0)) + (n - k) * (f[ 0 ][ 0 ]) - $不能传给 j 的方案数

接着考虑进一步优化:
对于sigma我们可以在上一层状态求一个 sum 来存储上一层的状态有多少特殊人传给 j 人的方案,然后加上 0 号人的,减去非法方案更新就完了。

至于滚动数组,我们可以吧f数组变成 \(f[ 2 ][ maxn ]\)\(f[ 0 ]\)表示上一层状态,\(f[ 1 ]\)表示这一层状态

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#define re register
using namespace std;
const int P = 998244353;
const int maxn=1e6+5,maxm=5e4+5;
int n,m,k;
int a[maxm],b[maxm],read[maxn],cnt=0,head[maxn],s[maxn];
long long f[2][maxn];
int num = 0;
struct node{
	int to,nxt;
}e[maxm];
inline void link(int u,int v){
	e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(re int i=1;i<=k;i++){
		scanf("%d%d",a+i,b+i);
		if(a[i]==b[i])continue;//需要判断
		read[++num]=a[i]; 
		read[++num]=b[i];
	}
	read[++num]=1;//1也算,他不可以随便传球
	sort(read+1,read+num+1);
	int Cnt=0;
	for(re int i=1;i<=num;i++){
		if(i==1 || read[i]!=read[i-1])s[++Cnt]=read[i];
	}
	for(re int i=1;i<=k;i++){//离散化 
		a[i]=lower_bound(s+1,s+1+Cnt,a[i])-s;
		b[i]=lower_bound(s+1,s+1+Cnt,b[i])-s;
		if(a[i]==b[i])continue;
		link(b[i],a[i]);
	}
	f[0][1] = 1;
	k = Cnt;//特殊人的个数
	long long sum = 1;
	for(re int i=1;i<=m;i++){//状态
		for(re int j=0;j<=k;j++)
			f[1][j]=(sum+1LL*(n-k)*f[0][0]%P)%P;
		sum=0;
		for(re int j=0;j<=k;j++){//决策
			for(re int k=head[j];k;k=e[k].nxt){//遍历不能传给他的人(反向建图的好处),减去非法方案,避免出现负数!!! 
				int v=e[k].to;
				f[1][j]=(f[1][j]-f[0][v]+P)%P;
			}
			f[1][j]=(f[1][j]-f[0][j]+P)%P;//自己不能传给自己
			if(j)sum=(sum+f[1][j])%P;
		}
		for(re int j=0;j<=k;j++){//滚动数组,为下一层状态做准备 
			f[0][j]=f[1][j];
			f[1][j]=0;
		}
	}
	cout<<f[0][1];
	return 0; 
}

PS:对于离散化操作,我们可以把所有特殊人存到一个有序的辅助空间 s 里,将其下标作为建图的下标即可

posted @ 2021-08-12 16:25  ¶凉笙  阅读(268)  评论(0编辑  收藏  举报