1. 程式人生 > 其它 >[題解] P5888 傳球遊戲

[題解] 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 裡,將其下標作為建圖的下標即可