P4099 [HEOI2013]SAO
题意
给你一个有向树形图,求拓扑序的个数。
Solution
本来想通过拓扑序直接跑 dp,发现假了,然后发现没有利用好这个树形图的性质。
既然是树形图,那我们先不考虑边的方向,当成一棵树。于是我们令 \(dp_{i,j}\) 表示以 \(i\) 为根的子树中拓扑序中 \(i\) 是第 \(j\) 个的方案数。然后考虑把 \(v\) 合并到根 \(v\) 里来。
那我们根据当前边的方向进行讨论:
- \(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}
\]
- \(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;
}