Loading

P4099 [HEOI2013]SAO

题意

给你一个有向树形图,求拓扑序的个数。

Solution

本来想通过拓扑序直接跑 dp,发现假了,然后发现没有利用好这个树形图的性质。

既然是树形图,那我们先不考虑边的方向,当成一棵树。于是我们令 \(dp_{i,j}\) 表示以 \(i\) 为根的子树中拓扑序中 \(i\) 是第 \(j\) 个的方案数。然后考虑把 \(v\) 合并到根 \(v\) 里来。

那我们根据当前边的方向进行讨论:

  1. \(u\to v\),也就是 \(u\) 的拓扑序必然在 \(v\) 之前,那么我们枚举一个 \(k\) 表示 \(v\) 中有多少点在 \(u\) 之前。假如说 \(i\)\(u\) 本来的拓扑序位置,\(j\)\(v\) 本来的拓扑序位置,那么经过转移 \(i\to i+k\),而这个 \(k\) 必须要满足 \(k<j\),然后用组合数算出两边合并的方案数,得到转移:

\[dp_{u,i+k}={i+k-1\choose k}\times dp_{u,i}\times dp_{v,j}\times {siz_u+siz_v-i-k\choose siz_v-k} \]

  1. \(v\to u\),也就是 \(u\) 的拓扑序必然在 \(v\) 之后。类似地我们可以得到一个差不多的转移式,只不过要求 \(k\ge j\)

对于上面这个 dp 的转移是 \(O(n^3)\) 的,但是由于枚举的时候其实中间只有一项和 \(j\) 有关,于是前后缀优化一下就可以了。

Code

#define int long long
using namespace std;
const int MAXN=1010;
const int MOD=1e9+7;
int C[MAXN][MAXN];
void init(){
	C[0][0]=1;rep(i,1,1000){
		C[i][0]=1; rep(j,1,1000)
		C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
	}
}//remember to call it
struct Edge{int to,fl;};
vector<Edge> e[MAXN];
int dp[MAXN][MAXN],tmp[MAXN],siz[MAXN],f[MAXN];
void dfs(int x,int fa){
	dp[x][1]=1;siz[x]=1;
	for(auto s:e[x]){
		if(s.to==fa) continue;
		int v=s.to; dfs(v,x);
		memcpy(f,dp[x],sizeof(f));
		memset(dp[x],0,sizeof(dp[x]));
		memset(tmp,0,sizeof(tmp));
		
		if(!s.fl) rep(j,1,siz[v]) tmp[j]=(tmp[j-1]+dp[v][j])%MOD;
		else per(j,siz[v],1) tmp[j]=(tmp[j+1]+dp[v][j])%MOD;
		
		per(i,siz[x],1) rep(k,0,siz[v]){
			int cur=C[i+k-1][k]*f[i]%MOD*C[siz[x]+siz[v]-i-k][siz[v]-k]%MOD;
			if(!s.fl) (cur*=tmp[k])%=MOD;
			else (cur*=tmp[k+1])%=MOD;
			(dp[x][i+k]+=cur)%=MOD;
		}
		siz[x]+=siz[v];
	}
}
void solve(){
	int n;cin>>n;
	rep(i,1,n-1){
		int u,v;char op;
		cin>>u>>op>>v;u++;v++;
		if(op=='>') swap(u,v);
		e[u].pb(Edge{v,1});
		e[v].pb(Edge{u,0});
	}dfs(1,0);
	int ans=0;
	rep(i,1,n) ans=(ans+dp[1][i])%MOD;
	cout<<ans<<'\n';
	memset(dp,0,sizeof(dp));
	rep(i,1,n) e[i].clear();
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	init();
	int T;for(cin>>T;T--;)solve();
	return 0;
}
posted @ 2022-07-26 21:34  ZCETHAN  阅读(29)  评论(0编辑  收藏  举报