树/森林的拓扑序计数总结 树形dp/换根dp 可重集排列

树/森林的拓扑序计数总结

树形dp/换根dp 可重集排列

i)有根树的拓扑序计数

对于有根树的拓扑序计数我们只需要做一次 自叶子节点向根节点 的 树形dp即可
首先先跑一遍 d f s dfs dfs 预处理好每个结点的 s z sz sz
定义 d p [ i ] dp[i] dp[i] i i i 结点子树的拓扑方案数
对于叶子节点的 d p dp dp 值显然是 1 1 1 假定已知 d p [ v ] , v ∈ s o n [ u ] dp[v],v∈son[u] dp[v],vson[u] 我们要求 d p [ u ] dp[u] dp[u]
u u u 结点的拓扑序显然是最前或最后 我们只需要考虑其子结点的位置排列即可
对于其子结点的顺序可以是任意的我们只需要保证来自同个子树的结点的相对顺序合法即可
即对于每个位置取来自其哪个子树的位置都可以但要保证相同子树的相对顺序合法
那么我们可以对每一种已确定的子树的排列方案进行可重集排序 意味着一共有

( s z [ u ] − 1 ) ! ∗ ∏ i , i ∈ s o n [ u ] (sz[u]-1)! * \prod\limits_{i,i∈son[u]} (sz[u]1)!i,ison[u] 1 s z [ i ] ! \frac{1}{sz[i]!} sz[i]!1 种排列方案

一共有 s z [ u ] − 1 sz[u]-1 sz[u]1 个结点 因为来自每个子树的结点的相对位置已经确定可以视为其结点是相同的故可使用可重集排列的方法处理 然而这是一种方案的排列方案 我们还需要乘上每个结点 即每个子树内部的方案数 d p [ i ] , i ∈ s o n [ u ] dp[i] ,i∈son[u] dp[i],ison[u] 故最终的转移方程为

d p [ u ] = ( s z [ u ] − 1 ) ! ∗ ∏ i , i ∈ s o n [ u ] dp[u]=(sz[u]-1)! * \prod\limits_{i,i∈son[u]} dp[u]=(sz[u]1)!i,ison[u] d p [ i ] s z [ i ] ! \frac{dp[i]}{sz[i]!} sz[i]!dp[i]

ii)多个有根树构成的森林的拓扑序计数

只需要加上一个超级源点并与每个有根树的根结点连边即可转化为单棵有根树的问题
附一例题 NC238725 - 至至子的公司排队

题目描述

至至子开有 n n n 家公司,第 i i i 家公司有 c i c_i ci 个员工,并且在该公司内形成了 c i − 1 c_i - 1 ci1 对直接上下级的关系(注意到 BOSS 是没有直接上级的)。今天他良心发现想让这 n n n 家公司的所有员工排队买票旅游。

然而这些人的等级观念很重——他们约定一个人在排队的时候不能排在其直接或间接上级的前面(若 a a a b b b 的直接上级或间接上级, b b b c c c 的直接/间接上级,则 a a a c c c 的间接上级)。

于是至至子很好奇,一共有多少种符合条件的排队方案呢?由于这个数可能很大,所以请输出答案对 1 0 9 + 7 10^9 + 7 109+7 取模的结果。

输入描述

第一行一个正整数 n n n,表示公司的数量,保证 1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105

接下来的 n n n 行每行描述一个公司,第一个正整数 c i c_i ci​ 表示第 i i i 个公司的人数。为了方便,我们将 同一公司 内的员工编号为 1 ∼ c i 1\sim c_i 1ci ,其中 1 1 1 为这个公司的 BOSS,没有直接上级。接下来的 c i − 1 c_i - 1 ci1 个正整数,分别描述 2 ∼ c i 2\sim c_i 2ci 号员工的直接上级 f i f_i fi 保证 1 ≤ f i ≤ c i 1\le f_i\le c_i 1fici,并且 ∑ i = 1 n c i ≤ 1 0 5 \sum\limits_{i=1}^n c_i\le 10^5 i=1nci105

输出描述

一行一个整数,表示答案对 1 0 9 + 7 10^9 + 7 109+7 取模的结果

输入样例1:

1
5 1 1 2 3

输出样例1:

6

样例1 说明:

2 2 2 3 3 3 的直接上司是 1 1 1 4 4 4 的直接上级是 2 2 2 5 5 5 的直接上级是 3 3 3 1 1 1 4 4 4 5 5 5 的间接上级。

6 6 6 种合法的排队方案分别为 ( 1 , 2 , 4 , 3 , 5 ) (1,2,4,3,5) (1,2,4,3,5) ( 1 , 3 , 5 , 2 , 4 ) (1,3,5,2,4) (1,3,5,2,4) ( 1 , 2 , 3 , 4 , 5 ) (1,2,3,4,5) (1,2,3,4,5) ( 1 , 3 , 2 , 4 , 5 ) (1,3,2,4,5) (1,3,2,4,5) ( 1 , 2 , 3 , 5 , 4 ) (1,2,3,5,4) (1,2,3,5,4) ( 1 , 3 , 2 , 5 , 4 ) (1,3,2,5,4) (1,3,2,5,4)。可以发现没有别的合法的排队方案。

输入样例2:

5
1
1
1
1
1

输出样例2:

120

样例2 说明:

5 5 5 家公司都只有各自的 BOSS,只考虑其排列方案即可,不难发现为 5 ! = 120 5! = 120 5!=120 种。

输入样例3:

3
2 1
4 1 2 3
4 1 2 2

输出样例3:

6300

输入样例4:

14
2 1
4 4 4 1
3 3 1
3 1 2
6 1 4 6 4 2
7 5 7 7 6 1 6
4 1 2 1
2 1
8 4 4 1 2 2 6 6
8 6 6 7 4 5 1 1
5 4 4 5 1
7 3 6 3 1 5 2
8 3 6 1 7 5 4 5
8 6 6 7 8 7 8 1

输出样例4:

430390195

题意

求多个有根树构成的森林的拓扑序方案数 对 1 0 9 + 7 10^9 + 7 109+7 取模

思路

每棵子树的序号转换成从 1 1 1 开始不重复的递增序号
加上一个超级源点 0 0 0 将其转化为求单棵有根树的拓扑序方案数

Code

#include<bits/stdc++.h>
using namespace std;
#define __T int csT;scanf("%d",&csT);while(csT--)
#define endl '\n'
const int mod=1e9+7;
const double PI= acos(-1);
const double eps=1e-6;
const int N=2e5+7;

int n,c,q,u,cnt;
struct node{
    int nx,to;
}e[100033];//记得多开几条边用于超级源点与每棵树根结点的连边
int h[100003];
long long dp[100003],jc[100003];
long long qpow(long long a,long long x)
{
    long long d=a,sum=1;
    while(x>0)
    {
        if(x%2==1)
        {
            sum=(sum*d)%mod;
        }
        x>>=1;
        d=(d*d)%mod;
    }
    return sum;
}
long long inv(long long x)
{
    return qpow(x,mod-2);
}
int sz[100003];
void szdfs(int at)
{
    sz[at]=1;
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        szdfs(to);
        sz[at]+=sz[to];
    }
}
void dpdfs(int at)
{
    dp[at]=jc[sz[at]-1];
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        dpdfs(to);
        dp[at]=(dp[at]*dp[to]%mod*inv(jc[sz[to]]))%mod;
    }
}
inline void sol()
{
    scanf("%d",&n);
    memset(h,-1,sizeof h);
    memset(sz,0,sizeof sz);
    q=cnt=0;
    for(int i=1;i<=n;++i)
    {
        scanf("%d",&c);
        e[++cnt].to=q+1;
        e[cnt].nx=h[0];
        h[0]=cnt;
        for(int j=2;j<=c;++j)
        {
            scanf("%d",&u);
            u=q+u;
            e[++cnt].to=q+j;
            e[cnt].nx=h[u];
            h[u]=cnt;
        }
        q+=c;
    }
    jc[0]=1;
    jc[1]=1;
    for(int i=2;i<=q;++i)jc[i]=(jc[i-1]*i)%mod;//预处理好模意义下的阶乘
    szdfs(0);//先求出每个结点的 size
    dpdfs(0);
    printf("%lld\n",dp[0]);
}
int main()
{
    sol();
    return 0;
}
iii)无根树的拓扑序计数

对于单个结点我们的操作和有根树是无异的 当然这对每个结点为根跑一遍树形 d p dp dp 显然是正确的但若此的时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的 这种暴力的时间复杂度大多数情况下是不能被接受的

于此 我们采用 换根 d p dp dp 去求解 利用已知正确答案的结点去推与之相邻结点的答案

对于已知答案的 d p [ u ] dp[u] dp[u] 对于与之相连的结点 v v v d p [ v ] dp[v] dp[v] 转移方程如下

d p [ v ] dp[v] dp[v] 既把 v v v 视为根结点 那么我们先将 d p [ u ] dp[u] dp[u] 的值更新为 v v v 为根结点意义下的值

而原本 v v v 的子树为 u u u 的子树故我们需要将其分离开 转移方程如下:

d p [ u ] = d p [ u ] ∗ ( s z [ u ] − s z [ v ] − 1 ) ! ( s z [ u ] − 1 ) ! ∗ s z [ v ] ! d p [ v ] dp[u]=dp[u]*\frac{(sz[u]-sz[v]-1)!}{(sz[u]-1)!}*\frac{sz[v]!}{dp[v]} dp[u]=dp[u](sz[u]1)!(sz[u]sz[v]1)!dp[v]sz[v]!

这里需要注意一个细节若将 d p [ x ] dp[x] dp[x] 保存为答案我们不能将已经求出答案的值给更新故每次求 v v v 意义下的 d p [ u ] dp[u] dp[u] 值时我们将其用一个新的变量储存起来避免其改变用于保存答案的 d p [ u ] dp[u] dp[u]
或是可以再定义一个 a n s [ x ] ans[x] ans[x] 数组去保存答案这样直接更新 d p [ u ] dp[u] dp[u] 亦无妨

更新好 v v v 意义下的 d p [ u ] dp[u] dp[u] 便可以更新 d p [ v ] dp[v] dp[v] 的值了 此时 u u u 的子树要加到 v v v 的子树中 转移方程如下:

d p [ v ] = d p [ v ] ∗ ( n − 1 ) ! ( s z [ v ] − 1 ) ! ∗ d p [ u ] ( s z [ u ] − s z [ v ] ) ! dp[v]=dp[v]*\frac{(n-1)!}{(sz[v]-1)!}*\frac{dp[u]}{(sz[u]-sz[v])!} dp[v]=dp[v](sz[v]1)!(n1)!(sz[u]sz[v])!dp[u]

然后更新一下新的 s z [ u ] = n − s z [ v ] sz[u]=n-sz[v] sz[u]=nsz[v], s z [ v ] = n sz[v]=n sz[v]=n 然后对 v v v 继续跑 d f s dfs dfs
注意跑完之后记得回溯 s z sz sz 以免在跑 u u u 结点的其他 v v v 结点时 s z sz sz 发生了变化

附一例题 F - Distributing Integers

Problem Statement

We have a tree with N N N vertices numbered 1 1 1 to N N N. The i − t h i-th ith edge in this tree connects Vertex a i a_i ai and b i b_i bi​ . For each k = 1 , . . . , N k=1,...,N k=1,...,N, solve the problem below:

  • Consider writing a number on each vertex in the tree in the following manner:
    • First, write 1 1 1 on Vertex k k k.
    • Then, for each of the numbers 2 , . . . , N 2,...,N 2,...,N in this order, write the number on the vertex chosen as follows:
      • Choose a vertex that still does not have a number written on it and is adjacent to a vertex with a number already written on it. If there are multiple such vertices, choose one of them at random.
  • Find the number of ways in which we can write the numbers on the vertices, modulo 1 0 9 + 7 10^9+7 109+7

Constraints

  • 2 ≤ N ≤ 2 × 1 0 5 2≤N≤2×10^5 2N2×105
  • 1 ≤ a i , b i ≤ N 1≤a_i,b_i≤N 1ai,biN
  • The given graph is a tree.

Input

Input is given from Standard Input in the following format:

N N N
a 1 a_1 a1 b 1 b_1 b1
:
a N − 1 a_N−1 aN1 b N − 1 b_N−1 bN1

Output

For each k = 1 , 2 , . . . , N k=1,2,...,N k=1,2,...,N in this order, print a line containing the answer to the problem.

Sample Input 1

3
1 2
1 3

Sample Output 1

2
1
1

The graph in this input is as follows:

3-1

For k=1, there are two ways in which we can write the numbers on the vertices, as follows:

  • Writing 1 , 2 , 3 1,2,3 1,2,3 on Vertex 1 , 2 , 3 1,2,3 1,2,3, respectively
  • Writing 1 , 3 , 2 1,3,2 1,3,2 on Vertex 1 , 2 , 3 1,2,3 1,2,3, respectively

Sample Input 2

2
1 2

Sample Output 2

1
1

The graph in this input is as follows:

3-2

Sample Input 3

5
1 2
2 3
3 4
3 5

Sample Output 3

2
8
12
3
3

The graph in this input is as follows:

3-3

Sample Input 4

8
1 2
2 3
3 4
3 5
3 6
6 7
6 8

Sample Output 4

40
280
840
120
120
504
72
72

The graph in this input is as follows:

3-4

题意

求单棵无根树每个结点作为根结点下的拓扑序方案数 对 1 0 9 + 7 10^9 + 7 109+7 取模

思路

先用 树形 d p dp dp 求出 以 1 1 1 作为根结点意义下的 拓扑序方案 然后用 换根 d p dp dp 求出其她点

Code

#include<bits/stdc++.h>//196 ms	
using namespace std;
#define __T int csT;scanf("%d",&csT);while(csT--)
#define endl '\n'
const int mod=1e9+7;
const double PI= acos(-1);
const double eps=1e-6;
const int N=2e5+7;

long long qpow(long long a,long long x)
{
    long long d=a,sum=1;
    while(x>0)
    {
        if(x%2==1)sum=(sum*d)%mod;
        x>>=1;
        d=(d*d)%mod;
    }
    return sum;
}
long long inv(long long x)
{
    return qpow(x,mod-2);
}

int n,u,v,cnt;
struct node{
    int to,nx;
}e[400003];
int h[200003];
long long jc[200003],dp[200003],sz[200003];

void szdfs(int at,int f)
{
    sz[at]=1;
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        szdfs(to,at);
        sz[at]+=sz[to];
    }
}
void dpdfs(int at,int f)
{
    dp[at]=jc[sz[at]-1];
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        dpdfs(to,at);
        dp[at]=(dp[at]*dp[to]%mod*inv(jc[sz[to]]))%mod;
    }
}
void hgdfs(int at,int f)
{
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        long long dpat=dp[at]*jc[sz[at]-1-sz[to]]%mod*inv(jc[sz[at]-1])%mod*jc[sz[to]]%mod*inv(dp[to])%mod;
        dp[to]=dp[to]*inv(jc[sz[to]-1])%mod*jc[n-1]%mod*dpat%mod*inv(jc[sz[at]-sz[to]])%mod;
        int szat=sz[at],szto=sz[to];
        sz[at]=n-sz[to];
        sz[to]=n;
        hgdfs(to,at);
        sz[at]=szat;
    }
}

inline void sol()
{
    scanf("%d",&n);
    jc[0]=1;
    jc[1]=1;
    for(int i=2;i<=n;++i)jc[i]=(jc[i-1]*i)%mod;//预处理出阶乘
    memset(h,-1,sizeof h);
    cnt=0;
    for(int i=1;i<=n-1;++i)
    {
        scanf("%d%d",&u,&v);
        e[++cnt].to=v;
        e[cnt].nx=h[u];
        h[u]=cnt;
        e[++cnt].to=u;
        e[cnt].nx=h[v];
        h[v]=cnt;
    }
    szdfs(1,0);//预处理出每个点的 size
    dpdfs(1,0);//求出以1作为根结点的方案数
    hgdfs(1,0);//由1开始跑换根dp
    for(int i=1;i<=n;++i)
    {
        printf("%lld\n",dp[i]);
    }
    puts("");
}
int main()
{
    sol();
    return 0;
}

这里给出一发朴素求逆元的版本 当然由于本题用到求逆元的形式都形如 x ! x! x! 故可以采用后缀积去线性预处理出阶乘的逆元 证明如下

首先先用费马小定理求出需要使用到的最大的阶乘的逆元 记为 i n v [ m a x ] inv[max] inv[max] 将其记为 n ! − 1 n!^{-1} n!1
其次我们要求 ( n − 1 ) ! (n-1)! (n1)! 的逆元 而 ( n − 1 ) ! ∗ n = n ! (n-1)!*n=n! (n1)!n=n! 故式子如下

( n − 1 ) ! ∗ n ∗ n ! = 1 ≡ 1 (n-1)!*n*n!^{=1}\equiv1 (n1)!nn!=11 m o d mod mod p p p

i n v [ ( n − 1 ) ! ] = i n v [ n ! ] ∗ n inv[(n-1)!]=inv[n!]*n inv[(n1)!]=inv[n!]n

invjc[n]=inv(jc[n]);
for(int i=n-1;i>=0;--i)
{
    invjc[i]=invjc[i+1]*(i+1)%mod;
}

将需要求阶乘的逆元的地方将预处理好的 i n v j c [ x ! ] invjc[x!] invjc[x!] 替换

#include<bits/stdc++.h>//115 ms
using namespace std;
#define __T int csT;scanf("%d",&csT);while(csT--)
#define endl '\n'
const int mod=1e9+7;
const double PI= acos(-1);
const double eps=1e-6;
const int N=2e5+7;

long long qpow(long long a,long long x)
{
    long long d=a,sum=1;
    while(x>0)
    {
        if(x%2==1)sum=(sum*d)%mod;
        x>>=1;
        d=(d*d)%mod;
    }
    return sum;
}
long long inv(long long x)
{
    return qpow(x,mod-2);
}

int n,u,v,cnt;
struct node{
    int to,nx;
}e[400003];
int h[200003];
long long jc[200003],dp[200003],sz[200003],invjc[200003];

void szdfs(int at,int f)
{
    sz[at]=1;
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        szdfs(to,at);
        sz[at]+=sz[to];
    }
}
void dpdfs(int at,int f)
{
    dp[at]=jc[sz[at]-1];
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        dpdfs(to,at);
        dp[at]=dp[at]*dp[to]%mod*invjc[sz[to]]%mod;
    }
}
void hgdfs(int at,int f)
{
    for(int i=h[at];~i;i=e[i].nx)
    {
        int to=e[i].to;
        if(e[i].to==f)continue;
        long long dpat=dp[at]*jc[sz[at]-1-sz[to]]%mod*invjc[sz[at]-1]%mod*jc[sz[to]]%mod*inv(dp[to])%mod;
        dp[to]=dp[to]*invjc[sz[to]-1]%mod*jc[n-1]%mod*dpat%mod*invjc[sz[at]-sz[to]]%mod;
        int szat=sz[at],szto=sz[to];
        sz[at]=n-sz[to];
        sz[to]=n;
        hgdfs(to,at);
        sz[at]=szat;
    }
}

inline void sol()
{
    scanf("%d",&n);
    jc[0]=1;
    for(int i=1;i<=n;++i)jc[i]=(jc[i-1]*i)%mod;
    invjc[n]=inv(jc[n]);
    for(int i=n-1;i>=0;--i)
    {
        invjc[i]=invjc[i+1]*(i+1)%mod;
    }
    memset(h,-1,sizeof h);
    cnt=0;
    for(int i=1;i<=n-1;++i)
    {
        scanf("%d%d",&u,&v);
        e[++cnt].to=v;
        e[cnt].nx=h[u];
        h[u]=cnt;
        e[++cnt].to=u;
        e[cnt].nx=h[v];
        h[v]=cnt;
    }
    szdfs(1,0);
    dpdfs(1,0);
    hgdfs(1,0);
    for(int i=1;i<=n;++i)
    {
        printf("%lld\n",dp[i]);
    }
    puts("");
}
int main()
{
    sol();
    return 0;
}
iiii)多个无根树构成的森林的拓扑序计数

这个 idea 先留着 :)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柯西可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值