1. 程式人生 > 實用技巧 >洛谷 P4099 - SAO

洛谷 P4099 - SAO

emmm... tzc 說是好題,那肯定是好題,因為 tzc 英語帶師!

洛谷題目頁面傳送門

給定一個基圖為樹的有向圖,求它的拓撲序個數。答案對 \(10^9+7\) 取模。本題多測。

\(n\in[1,1000],T\in[1,5]\)

考慮樹形 DP。很自然的想到 \(dp_{i,j}\) 表示子樹 \(i\) 中節點 \(i\) 在拓撲序列中的序數為 \(j\) 的拓撲序列數。

那麼在轉移的時候,要把一個節點的所有兒子都考慮進去,這個極不好控制,是指數級的。不難發現,兒子與兒子之間沒有直接聯絡,只是 \(i\) 與兒子們各有一個聯絡。於是可以將兒子一個一個算入轉移中,而非同時轉移,這樣能使時間複雜度得以控制在多項式級別。具體地,大概就加個一維 \(dp_{i,j,k}\)

\(j\) 表示考慮到 \(i\) 的第 \(j\) 個兒子了,\(k\) 為原 \(j\)。這樣看起來狀態數增加了一級,其實不然,因為 \(\sum\limits_{i=1}^n|son_i|=\mathrm O(n)\)。還是 \(\mathrm O\!\left(n^2\right)\)

目標:\(\sum\limits_{i=1}^ndp_{1,|son_1|,i}\);邊界:\(dp_{i,0,1}=1\)

接下來考慮如何轉移。當前要計算 \(dp_{i,j}\),我們設 \(h=dp_{i,j},f=dp_{i,j-1},g=dp_{son_{i,j},\left|son_{son_{i,j}}\right|}\)

,那麼顯然是 \(f\)\(g\) 共同算出 \(h\)。然後設 \(sz_h,sz_f,sz_g\) 分別為所對應的 size。根據 \(i\)\(son_{i,j}\) 之間邊的方向,分兩種情況,分別為在拓撲序中 \(son_{i,j}\)\(i\) 前和後。現在先討論前者。

考慮列舉 \(h\) 中在 \(i\) 前面有幾個 \(g\) 中的點。那麼此時 \(i\)\(f\) 中的排名已經確定了,用在 \(h\) 中的排名減去前面在 \(g\) 中的點數即可。那麼 \(son_{i,j}\)\(g\) 中的排名呢?它需要在 \(i\) 前面,於是在 \(1\)

到「\(h\) 中在 \(i\) 前面有幾個 \(g\) 中的點」均可。\(f\)\(g\) 內部的排列方案數定下來了,然後再很自然地給 \(f\)\(g\) 分配相對位置,\(i\) 左邊和右邊各乘上一個組合數即可。於是得到前者的狀態轉移方程:

\[h_i=\sum_{j=1}^{sz_g}f_{i-j}\sum_{k=1}^jg_k\binom{i-1}j\binom{sz_h-i}{sz_g-j} \]

類似地可以得到後者的方程(比較噁心一點):

\[h_i=\sum_{j=1}^{sz_g}f_{i+j-sz_g}\sum_{k=sz_g-j+1}^{sz_g}g_k\binom{sz_h-i}j\binom{i-1}{sz_g-j} \]

此時總時間複雜度是 \(\mathrm O\!\left(n^4\right)\) 的(平方狀態,平方轉移)。考慮優化。

顯然,兩個方程中的第二個 \(\sum\) 可以用字首和優化掉,此時三方,繼續優化。然後定睛一看:寫成字首和形式之後,展開組合數,發現一派常數一派關於 \(j\) 一派關於 \(i-j\),那不就是個卷積嗎?!我當時害怕極了,我不會 FFT 呀!而且即使優化了,也是平方對數,還要乘個 \(T\),也是過不去的樣子。

然後發現這個三方的複雜度很虛偽,它實際上是 \(\mathrm O\!\left(\sum\limits_{i=1}^n\sum\limits_{j=1}^{|son_i|}sz_{son_{i,j}}\sum\limits_{k=1}^{j-1}sz_{son_{i,k}}\right)\),很跑不滿三方。然後拿鏈、菊花、蒲公英、毛毛蟲、隨機樹都試一遍,發現沒有一個被卡掉的。事實上它是平方的。下面歸納證明 \(\mathrm T(n)\leq n^2\)

  1. 顯然 \(\mathrm T(0)=0\)
  2. 假設 \(\forall i\in[0,x),\mathrm T(i)\leq i^2\)。設大小為 \(x\) 的樹的樹根的兒子子樹大小分別為 \(a_{1\sim m}\),則顯然有 \(\mathrm T(x)=\sum\limits_{i=1}^m\mathrm T(a_i)+\sum\limits_{i=1}^ma_i\sum\limits_{j=1}^{i-1}a_j\)。根據假設有 \(\mathrm T(x)\leq \sum\limits_{i=1}^ma_i^2+\sum\limits_{i=1}^ma_i\sum\limits_{j=1}^{i-1}a_j=\sum\limits_{i=1}^ma_i\sum\limits_{j=1}^ia_j\leq x^2\)

綜上,得證。

程式碼(要很多次取模,常數比較大,要開 \(\mathrm O_2\)):

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const int mod=1000000007;
const int N=1000;
int n;
int comb[N+1][N+1];
vector<pair<int,bool> > nei[N+1];
int dp[N+1][N+2];
int nw[N+1];
int sz[N+1];
void dfs(int x=1,int fa=0){
	sz[x]=1;
	dp[x][1]=1;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i].X;bool z=nei[x][i].Y;
		if(y==fa)continue;
		dfs(y,x);
		sz[x]+=sz[y];
		if(z){
			for(int j=1;j<=sz[y];j++)(dp[y][j]+=dp[y][j-1])%=mod;
			for(int j=1;j<=sz[x];j++)nw[j]=0;
			for(int j=1;j<=sz[x];j++)for(int k=0;k<=sz[y]&&k<j;k++)
				(nw[j]+=1ll*dp[x][j-k]*dp[y][k]%mod*comb[j-1][k]%mod*comb[sz[x]-j][sz[y]-k]%mod)%=mod;
			for(int j=1;j<=sz[x];j++)dp[x][j]=nw[j];
		}
		else{
			for(int j=sz[y];j;j--)(dp[y][j]+=dp[y][j+1])%=mod;
			for(int j=1;j<=sz[x];j++)nw[j]=0;
			for(int j=1;j<=sz[x];j++)for(int k=0;k<=sz[y]&&k<=sz[x]-j;k++)
				(nw[j]+=1ll*dp[x][j+k-sz[y]]*dp[y][sz[y]-k+1]%mod*comb[sz[x]-j][k]%mod*comb[j-1][sz[y]-k]%mod)%=mod;
			for(int j=1;j<=sz[x];j++)dp[x][j]=nw[j];
		}
	}
}
void mian(){
	cin>>n;
	for(int i=1;i<=n;i++)nei[i].clear();
	memset(dp,0,sizeof(dp));
	for(int i=1;i<n;i++){
		int x,y;char z;
		cin>>x>>z>>y;
		x++,y++;
		if(z=='<')nei[x].pb(mp(y,0)),nei[y].pb(mp(x,1));
		else nei[x].pb(mp(y,1)),nei[y].pb(mp(x,0));
	}
	dfs();
	int ans=0;
	for(int i=1;i<=n;i++)(ans+=dp[1][i])%=mod;
	cout<<ans<<"\n";
}
int main(){
	comb[0][0]=1;
	for(int i=1;i<=N;i++)for(int j=0;j<=i;j++)comb[i][j]=((j?comb[i-1][j-1]:0)+(j<i?comb[i-1][j]:0))%mod;
	int testnum;
	cin>>testnum;
	while(testnum--)mian();
	return 0;
}