我们先不考虑边的权值(< 与>),这样子 $n-1$ 条边组成的就是树了,很显然是需要我们求出这棵树的合法拓扑序的个数,考虑使用 $\rm{DP}$ ,对于边的方向(即<,>) ,我们分类讨论即可。

首先的一个想法就是设 $f_u$ 表示点 $u$ 的子树的合法拓扑序的总数,但是这个时候如何计算呢,对于一个 $u$ 的儿子 $v$ ,我们虽然知道 $u$ 和 $v$ 的攻克的前后关系,但是合并答案貌似并不好合并。这个时候我们增加一维 $j$ ,$f_{u,j}$ 表示 $u$ 的子树的所有合法拓扑序中 $u$ 在第 $j$ 位上的总状态数。

也就是说,对于一个必须在 $u$ 前面攻克的关卡 $v$ ,我们考虑枚举一个 $j$ ,$v$ 子树中 $j$ 个结点在合并 $u,v$ 后放到 $u$ 前面,另外 $sz_v-j$ 个放到 $u$ 后面,然后枚举一个 $k$ ,表示当前的 $v$ 排在 $v$ 子树的拓扑序中的第 $k$ 位,只有 $k\leq j$ 的时候 $v$ 才可以转移 $u$ ,因为这个时候 $v$ 在 $u$ 前面。

现在再来考虑 $“$ $j$ 个结点放在 $u$ 前面 $”$ 的方案数和 $“$ $sz_v-j$ 个结点放在 $u$ 后面的方案数 $”$,这个显然可以用组合数算,合并 $v$ 的子树后,$u$ 的排名从 $i$ 变成了 $i+j$ ,也就是说我们需要将 $j$ 个乱序插入到 $u$ 前面 $i+j-1$ 个数中,方案数显然为 $C_{i+j-1}^{j}$ ,那么现在总节点数显然为 $sz_u+sz_v$ (现在 $sz_u$ 和 $sz_v$ 还没有并在一起) ,$u$ 后面理所当然有 $sz_u+sz_v-i-j$ 个位置,将 $sz_v-j$ 个数插进去的方案数显然为 $C_{sz_u+sz_v-i-j}^{sz_v-j}$ 个,这两个数再乘上 $f_{u,i}$ 和 $f_{v,k}$ 就好了,这一次合并后 $u$ 的位置显然到了 $i+j$ ,所以 $f_{u,i+j}$ 显然要加上这一组贡献。

经整理后的转移方程如下:

$$pls(f_{u,i+j},f_{u,i}\cdot f_{v,k}\cdot C_{i+j-1}^{j}\cdot C_{sz_u+sz_v-i-j}^{sz_v-j})$$

代码就是这样写:

for i=1 to sz[u]
    for j=1 to sz[v]
        for k=1 to j
            pls(f[u][i+j],f[u][i]*f[v][k]*C[i+j-1][j]*C[sz[u]+sz[v]-i-j][sz[v]-j])

这是 $n^3$ 的,过不去。考虑前缀和优化,几下 $f_v$ 的前缀和,最后的一层循环就可以直接丢掉了。

这个就是 $v$ 要在 $u$ 前面的情况,$u$ 在 $v$ 前面的情况和这个差不多,不过转移的时候 $j$ 就要从 $0$ 开始了,因为那个时候 $u$ 前面是可以不多放任何东西的,还有就是 $u$ 在 $v$ 前面的时候注意 $k\geq j$ 时才可以转移 !

最后的答案就是 $\sum\limits_{i=1}^{n} f_{1,i}$ 啦。

Code:

#include <queue>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N=1e3+9;
const int mod=1e9+7;

int head[N],cnt;
struct Edge{int nxt,to,val;}G[N<<1];
int C[N][N],f[N][N],pre[N][N],suf[N][N],sz[N],g[N];

void add(int u,int v,int w) {
    G[++cnt]=(Edge){head[u],v,w},head[u]=cnt;
    G[++cnt]=(Edge){head[v],u,w^1},head[v]=cnt;
}

namespace OI {
    void pls(int&x,int y) {x+=y;if(x>=mod)x-=mod;}
    template <typename _Tp> inline void IN(_Tp&x) {
        char ch;bool flag=0;x=0;
        while(ch=getchar(),!isdigit(ch)) if(ch=='-') flag=1;
        while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
        if(flag) x=-x;
    }
}using namespace OI;

void dfs(int u,int fa) {
    sz[u]=f[u][1]=1;
    for(int l=head[u];l;l=G[l].nxt) {
        int v=G[l].to,w=G[l].val;
        if(v==fa) continue;
        dfs(v,u);
        memset(g,0,sizeof(g));
        if(w) {
            for(int i=1;i<=sz[u];++i)
                for(int j=1;j<=sz[v];++j)
                    pls(g[i+j],1ll*f[u][i]*pre[v][j]%mod*C[i+j-1][j]%mod
                    *C[sz[u]+sz[v]-i-j][sz[v]-j]%mod);
        } else {
            for(int i=1;i<=sz[u];++i)
                for(int j=0;j<=sz[v];++j)
                    pls(g[i+j],1ll*f[u][i]*suf[v][j+1]%mod*C[i+j-1][j]%mod
                    *C[sz[u]+sz[v]-i-j][sz[v]-j]%mod);
        }
        sz[u]+=sz[v];
        memcpy(f[u],g,sizeof(g));
    }
    pre[u][0]=suf[u][sz[u]+1]=0;
    for(int i=1;i<=sz[u];++i) pre[u][i]=(pre[u][i-1]+f[u][i])%mod;
    for(int i=sz[u];i>=1;--i) suf[u][i]=(suf[u][i+1]+f[u][i])%mod;
}

int solve() {
    memset(head,0,sizeof(head)),cnt=0;
    memset(f,0,sizeof(f));
    int n;IN(n);
    for(int i=1;i<n;++i) {
        int u,v;char sign;
        IN(u),sign=getchar(),IN(v);
        add(u+1,v+1,sign=='<'?0:1);
    }
    dfs(1,0);
    int ans=0;
    for(int i=1;i<=n;++i) pls(ans,f[1][i]);
    return ans;
}

int main() {
    /*预处理组合数*/
    for(int i=0;i<=N-2;++i) C[i][0]=1;
    for(int i=1;i<=N-2;++i)
        for(int j=1;j<=N-2;++j)
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
    int T;IN(T);
    while(T--) printf("%d\n",solve());
    return 0;
}

可能有人会问,如果 $u$ 的儿子 $v$ 下面的边全都是 $>$ ,并且 $u$ 连向 $v$ 的边也是 $>$ ,那么这个时候 $v$ 以及其子树的所有点都必须在 $u$ 前面完成,在转移的时候为什么可以 $“$ 提出 $j$ 个结点放到 $u$ 后面 $”$ 呢?

其实想想就可以明白,在向上统计答案的时候对于一个 $v$ 的儿子 $a$ ,我们只统计了合并后 $a$ 在 $v$ 前面的情况,同样在 $u$ 统计 $v$ 时也只是统计了合并后 $v$ 在 $u$ 前面的情况,所有我们也只是统计了 $“$ $a$ 在 $v$ 前面且 $v$ 在 $u$ 前面 $”$ 的情况,所有被统计的情况一定是合法的。

分类: 文章

Qiuly

QAQ

0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注