[WC2019] 数树 容斥原理+矩阵树定理+树形Dp+计数Dp+生成函数优化Dp+多项式求Exp

[WC2019] 数树

题目传送门

分析

最近老是在肝一些神仙生成函数题。。。哎,肝败吓疯。其实luogu题解里面的那篇已经很详细了,这篇题解纯属个人整理,建议是到到luogu题解去看。
题目大意:告诉你有俩棵有标号无根树,如果某两个节点共用了某条边,那么这两个点的权值必须相同,点权范围在 [ 1 , y ] [1,y] [1,y]内,有三个任务,求在给定2,1,0棵树的情况下构造树和点权的方案数。

Task0:简单转化

如果两棵树都给的话,就是把都存在的边放在图中,假设有 c n t cnt cnt个连通块,答案就是 y c n t y^{cnt} ycnt,因为是树,所以答案就是 y n − m y^{n-m} ynm,其中 m m m是边数。

Task1:各种套路

现在少了一棵树,这个时候我们就要考虑怎么形式化问题。

不难发现,由于答案只和重合的边条数有关系,所以最简单的想法是枚举重合的边集。

A n s = ∑ S F ( S ) y n − ∣ S ∣ Ans=\sum\limits_SF(S)y^{n-|S|} Ans=SF(S)ynS

其中 F ( S ) F(S) F(S)表示与初始树重合边集恰好 S S S的方案数。

看到我加粗了恰好两个字就知道我要干什么了对吗:-)

套路1:容斥原理

C ( T ) C(T) C(T)表示重合的边集包含 T T T的方案数。

根据容斥原理,可以得到:

F ( S ) = ∑ S ⊆ T ( − 1 ) ∣ T ∣ − ∣ S ∣ C ( T ) F(S)=\sum\limits_{S\subseteq T} (-1)^{|T|-|S|}C(T) F(S)=ST(1)TSC(T)

套路2:交换求和

这个时候带回原式化简一波

A n s = ∑ S F ( S ) y n − ∣ T ∣ = ∑ S ∑ S ⊆ T ( − 1 ) ∣ T ∣ − ∣ S ∣ C ( T ) y n − ∣ S ∣ = y n ∑ T ( − 1 ) ∣ T ∣ C ( T ) ∑ S ⊆ T ( − y ) − ∣ S ∣ Ans=\sum\limits_SF(S)y^{n-|T|}=\sum\limits_S\sum\limits_{S\subseteq T} (-1)^{|T|-|S|}C(T)y^{n-|S|}=y^n\sum\limits_T(-1)^{|T|}C(T)\sum\limits_{S\subseteq T} (-y)^{-|S|} Ans=SF(S)ynT=SST(1)TSC(T)ynS=ynT(1)TC(T)ST(y)S

发现后面那坨仅仅和集合大小有关系,我们枚举 T T T子集的大小 i i i,那么答案就是

A n s = y n ∑ T ( − 1 ) ∣ T ∣ C ( T ) ∑ i C ∣ T ∣ i ( − y ) i = y n ∑ T ( − 1 ) ∣ T ∣ C ( T ) ( 1 − 1 y ) ∣ T ∣ Ans=y^n\sum\limits_T(-1)^{|T|}C(T)\sum_i C_{|T|}^i(-y)^i=y^n\sum\limits_T(-1)^{|T|}C(T)(1-\frac{1}{y})^{|T|} Ans=ynT(1)TC(T)iCTi(y)i=ynT(1)TC(T)(1y1)T

于是我们得到了重要结论:

A n s = y n ∑ T C ( T ) ( 1 y − 1 ) ∣ T ∣ Ans=y^n\sum\limits_TC(T)(\frac{1}{y}-1)^{|T|} Ans=ynTC(T)(y11)T

然而到目前为止,复杂度仍然是指数级的,瓶颈在于 C ( T ) C(T) C(T),因此我们要继续形式化 C ( T ) C(T) C(T)

套路3:矩阵树定理

考虑如果我把 T T T给你,要怎么求 C ( T ) C(T) C(T)

考虑模型化问题,实际上就是钦定了若干条边,要求任意连边求生成树个数。

T T T中的一个连通块,显然不能再连边,因此将他们缩成一个点,任意两个连通块实际上有连通块大小乘积种连边方案。所以假设 T T T中的联通块大小分别为 a 1 , a 2 ⋯ a k a_1,a_2\cdots a_k a1,a2ak,对于任意两个连通块 i , j i,j i,j,我们连 a i a j a_ia_j aiaj条边,形成的图的生成树个数就是 C ( T ) C(T) C(T)

但是我们还是无法避免枚举 C ( T ) C(T) C(T),所以我们还得继续挖掘 C ( T ) C(T) C(T)的性质。索性,将 K i r c h h o f f Kirchhoff Kirchhoff矩阵拿出来玩玩。
[ a 1 ( n − a 1 ) − a 1 a 2 ⋯ − a 1 a k − a 1 a 2 a 2 ( n − a 2 ) ⋯ − a 2 a k ⋮ ⋮ ⋱ ⋯ − a k a 1 − a k a 2 ⋯ a k ( n − a k ) ] \left[ \begin{matrix} a_1(n-a_1) & -a_1a_2 & \cdots & -a_1a_k \\ -a_1a_2 & a_2(n-a_2) & \cdots & -a_2a_k \\ \vdots & \vdots & \ddots &\cdots \\ -a_ka_1 & -a_ka_2 &\cdots & a_k(n-a_k) \end{matrix} \right] a1(na1)a1a2aka1a1a2a2(na2)aka2a1aka2akak(nak)

套路4:手玩行列式

先去掉一行一列,得到
∣ a 1 ( n − a 1 ) − a 1 a 2 ⋯ − a 1 a k − 1 − a 2 a 1 a 2 ( n − a 2 ) ⋯ − a 2 a k − 1 ⋮ ⋮ ⋱ ⋯ − a k − 1 a 1 − a k − 1 a 2 ⋯ a k − 1 ( n − a k − 1 ) ∣ \left| \begin{matrix} a_1(n-a_1) & -a_1a_2 & \cdots & -a_1a_{k-1} \\ -a_2a_1 & a_2(n-a_2) & \cdots & -a_2a_{k-1} \\ \vdots & \vdots & \ddots &\cdots \\ -a_{k-1}a_1 & -a_{k-1}a_2 &\cdots & a_{k-1}(n-a_{k-1}) \end{matrix} \right| a1(na1)a2a1ak1a1a1a2a2(na2)ak1a2a1ak1a2ak1ak1(nak1)
发现其实每行都有一个 a i a_i ai可以提一个 ∏ i k − 1 a i \prod_i^{k-1}a_i ik1ai。得到
∣ ( n − a 1 ) − a 2 ⋯ − a k − 1 − a 1 ( n − a 2 ) ⋯ − a k − 1 ⋮ ⋮ ⋱ ⋯ − a 1 − a 2 ⋯ ( n − a k − 1 ) ∣ \left| \begin{matrix} (n-a_1) & -a_2 & \cdots & -a_{k-1} \\ -a_1 & (n-a_2) & \cdots & -a_{k-1} \\ \vdots & \vdots & \ddots &\cdots \\ -a_1 & -a_2 &\cdots & (n-a_{k-1}) \end{matrix} \right| (na1)a1a1a2(na2)a2ak1ak1(nak1)
发现每行的和都是 n − ∑ i k − 1 a i = a k n-\sum_i^{k-1}a_i=a_k nik1ai=ak,因此把 2 ⋯ k − 1 2\cdots k-1 2k1列加到第 1 1 1列上,得到
∣ a k − a 2 ⋯ − a k − 1 a k ( n − a 2 ) ⋯ − a k − 1 ⋮ ⋮ ⋱ ⋯ a k − a 2 ⋯ ( n − a k − 1 ) ∣ \left| \begin{matrix} a_k & -a_2 & \cdots & -a_{k-1} \\ a_k & (n-a_2) & \cdots & -a_{k-1} \\ \vdots & \vdots & \ddots &\cdots \\ a_k & -a_2 &\cdots & (n-a_{k-1}) \end{matrix} \right| akakaka2(na2)a2ak1ak1(nak1)
这样的话,每一列除了对角线上的元素都是相同的。考虑将每行减去第一行,就得到了一个上三角了。
∣ a k − a 2 ⋯ − a k − 1 0 n ⋯ 0 ⋮ ⋮ ⋱ ⋯ 0 0 ⋯ n ∣ \left| \begin{matrix} a_k & -a_2 & \cdots & -a_{k-1} \\ 0 & n & \cdots & 0 \\ \vdots & \vdots & \ddots &\cdots \\ 0 & 0 &\cdots & n \end{matrix} \right| ak00a2n0ak10n
手玩了一阵子的行列式之后,发现答案实际上就是 C ( T ) = n k − 2 ∏ i k a i C(T)=n^{k-2}\prod_i^{k}a_i C(T)=nk2ikai,其中 k k k是连通块个数, a i a_i ai是连通块大小。

因为 T T T是树上的边集,所以连通块的个数 k = n − ∣ T ∣ k=n-|T| k=nT

进一步带入化简可以得到 A n s = y n ∑ T n n − ∣ T ∣ − 2 ∏ i n − ∣ T ∣ a i ( 1 y − 1 ) ∣ T ∣ Ans=y^n\sum\limits_Tn^{n-|T|-2}\prod_i^{n-|T|}a_i(\frac{1}{y}-1)^{|T|} Ans=ynTnnT2inTai(y11)T

为了方便起见,我们设 p = 1 y − 1 p=\frac{1}{y}-1 p=y11

考虑将 p p p n n n分配进去

A n s = y n p n n 2 ∑ T ∏ i n − ∣ T ∣ n a i p Ans=\frac{y^np^n}{n^2}\sum\limits_T\prod_i^{n-|T|}\frac{na_i}{p} Ans=n2ynpnTinTpnai

k = n p k=\frac{n}{p} k=pn,现在问题转化成了,将一棵树划分成若干个连通块,每个连通块的贡献是 k k k乘上连通块大小,一个划分的权值是所有连通块贡献之积,求所有划分权值之和。

这个问题显然可以用一个 D p Dp Dp解决

套路5:生成函数优化Dp

这个操作我是第一次见。。。。

首先考虑上面的 D p Dp Dp,假设 f u , i f_{u,i} fu,i表示以 u u u为根的子树, u u u所在连通块大小为 i i i(未计入答案)的划分权值之和。

考虑子树合并贡献方程,假设合并了一个 v v v子树。
{ f u , i f v , j → f u , i + j ∗ k f u , i f v , j j → f u , i ∗ \left\{ \begin{aligned} f_{u,i}f_{v,j} &\to f^*_{u,i+j} \\ kf_{u,i}f_{v,j}j &\to f^*_{u,i} \end{aligned} \right. {fu,ifv,jkfu,ifv,jjfu,i+jfu,i
两个方程分别对应切和不切。我们终于得到了一个 O ( n 2 ) O(n^2) O(n2)的算法!

接下来就应该优化这个方程。

这个时候考虑方程的生成函数:

f u ( z ) = ∑ i f u , i z i f_u(z)=\sum_i f_{u,i}z^i fu(z)=ifu,izi

方程可以被简单地写成

f u ∗ ( z ) = f u f v + k f u f v ′ ( 1 ) f^*_u(z)=f_uf_v+kf_uf_v'(1) fu(z)=fufv+kfufv(1)

然后树链剖分+NTT就可以做到两个log

这个时候考虑我们需要的答案是什么?

∑ k f 1 , i i = k f 1 ′ ( 1 ) \sum kf_{1,i}i=kf_1'(1) kf1,ii=kf1(1)

答案的形式如此简单,因此我们尝试在转移中仅仅转移答案。

f u ∗ ′ = f u ′ f v + f u f v ′ + f u ′ f ′ v ( 1 ) f^{*'}_u=f_u'f_v+f_uf_v'+f_u'f'v(1) fu=fufv+fufv+fufv(1)

k f u ∗ ′ ( 1 ) = k f u ′ ( 1 ) f v ( 1 ) + f u ( 1 ) k f v ′ ( 1 ) + k f u ′ ( 1 ) k f ′ v ( 1 ) kf^{*'}_u(1)=kf_u'(1)f_v(1)+f_u(1)kf_v'(1)+kf_u'(1)kf'v(1) kfu(1)=kfu(1)fv(1)+fu(1)kfv(1)+kfu(1)kfv(1)

g u = k f u ′ ( 1 ) , t u = f u ( 1 ) g_u=kf_u'(1),t_u=f_u(1) gu=kfu(1),tu=fu(1)

{ g u ∗ = g u t v + t u g v + g u g v t u ∗ = t u t v + g v t u \left\{ \begin{aligned} g_u^* &=g_ut_v+t_ug_v+g_ug_v \\ t_u^* &=t_ut_v+g_vt_u \end{aligned} \right. {gutu=gutv+tugv+gugv=tutv+gvtu
我们得到了一个 O ( n ) O(n) O(n)的优秀树 D p Dp Dp!经过重重套路,终于解决了 T a s k 1 Task1 Task1

最终的答案是 A n s = y n p n n 2 g 1 Ans=\frac{y^np^n}{n^2}g_1 Ans=n2ynpng1

Task2:模型使用

发现其实 T a s k 1 Task1 Task1中的容斥是可以用滴!

A n s = y n ∑ T C 2 ( T ) p ∣ T ∣ = y n ∑ T n 2 n − 2 ∣ T ∣ − 4 ∏ i 2 n − 2 ∣ T ∣ a i p ∣ T ∣ = y n p 2 n n 4 ∏ i 2 n − 2 ∣ T ∣ n 2 p a i 2 Ans=y^n\sum\limits_TC^2(T)p^{|T|}=y^n\sum\limits_Tn^{2n-2|T|-4}\prod\limits_i^{2n-2|T|}a_ip^{|T|}=\frac{y^np^{2n}}{n^4}\prod\limits_i^{2n-2|T|}\frac{n^2}{p}a_i^2 Ans=ynTC2(T)pT=ynTn2n2T4i2n2TaipT=n4ynp2ni2n2Tpn2ai2

我们同样令 k = n 2 p k=\frac{n^2}{p} k=pn2。但是却不能再采用上一题的方法,因为这里的 T T T是任意一个合法的森林的边集。这和上一题一颗树的子边集大不相同。因此考虑采用模型转化。

梳理一下问题:

某个连通块的权值为其大小的平方

若干个连通块组成的图的权值是各个连通块的权值积

n n n个点的所有不同森林的权值和

这是一个经典的模型。将连通块看成一个集合,那么就成为了若干个关于集合大小的自由组合问题。

考虑一个大小为 i i i的集合的指数型生成函数: f ( x ) = a ( i ) x i i ! f(x)=a(i)\frac{x^i}{i!} f(x)=a(i)i!xi

和答案大小为 i i i个点的指数型生成函数: g ( x ) = b ( i ) x i i ! g(x)=b(i)\frac{x^i}{i!} g(x)=b(i)i!xi

g ( x ) = e f ( x ) g(x)=e^{f(x)} g(x)=ef(x)

原因是相当于把若干个不同大小的集合拼在一起,再消除内部顺序的影响。

对应这道题,大小为 i i i的树的方案数有 i i − 2 i^{i-2} ii2中方案,每种方案的权值都是 i 2 i^2 i2

所以 a ( i ) = i i − 2 i 2 = i i a(i)=i^{i-2}i^2=i^i a(i)=ii2i2=ii

那么构造 f ( x ) = i i x i i ! f(x)=i^i\frac{x^i}{i!} f(x)=iii!xi

g ( x ) = e f ( x ) g(x)=e^f(x) g(x)=ef(x)

最后的答案就是 A n s = y n p 2 n n 4 b ( n ) Ans=\frac{y^np^{2n}}{n^4}b(n) Ans=n4ynp2nb(n)

本题层次分明,形成了题面,解法的统一和思维层次的不断螺旋上升,但却有章法可询,虽然难点众多,但却可以层层分析,层层推导,是难得一见的好题!(Call爆它)

代码

沉迷封装,无法自拔。

#include<bits/stdc++.h>
const int N = 524288, P = 998244353;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int n, y;
int fix(int x) {return (x >> 31 & P) + x;}
int Pow(int x, int k) {
    int r = 1;
    for(;k; x = 1LL * x * x % P, k >>= 1)
        if(k & 1)
            r = 1LL * r * x % P;
    return r;
}
int Inv(int x) {return Pow(x, P - 2);}
namespace Solve0 {
    std::map<long long, bool> mp;
    void Work() {
        if(y == 1) return printf("%d\n", Pow(n, n - 2)), void();
        int cnt = n;
        for(int i = 1;i < n; ++i) {
            int u = ri(), v = ri();
            if(u > v) std::swap(u, v);
            mp[1LL * u * n + v] = true;
        }
        for(int i = 1;i < n; ++i) {
            int u = ri(), v = ri();
            if(u > v) std::swap(u, v);
            if(mp.count(1LL * u * n + v))
                --cnt;
        }
        printf("%d\n", Pow(y, cnt));
    }
}
namespace Solve1 {
    int t[N], g[N], k, p, pr[N], to[N], nx[N], tp;
    void add(int u, int v) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp;}
    void adds(int u, int v) {add(u, v); add(v, u);}
    void Dp(int u, int fa) {
        t[u] = 1; g[u] = k;
        for(int i = pr[u]; i; i = nx[i])
            if(to[i] != fa) {
                Dp(to[i], u); int res = t[to[i]] + g[to[i]];
                g[u] = (1LL * g[u] * res + 1LL * t[u] * g[to[i]]) % P;
                t[u] = 1LL * t[u] * res % P;
            }
    }
    void Work() {
        if(y == 1) return printf("%d\n", Pow(n, (n - 2) % (P - 1))), void();
        for(int i = 1;i < n; ++i)
            adds(ri(), ri());
        p = Inv(y) - 1;
        k = 1LL * n * Inv(p) % P;
        Dp(1, 0);
        printf("%d\n", 1LL * Pow(P + 1 - y, n) * Pow(n, P - 3) % P * g[1] % P);
    }
}
namespace Solve2 {
    typedef std::vector<int> VI;
    int L, InvL, R[N], w[N];
    void Pre(int m) {
        int x = 0; L = 1;
        for(;(L <<= 1) < m;) ++x;
        for(int i = 1;i < L; ++i)
            R[i] = R[i >> 1] >> 1 | (i & 1) << x;
        int wn = Pow(3, (P - 1) / L); w[0] = 1;
        for(int i = 1;i < L; ++i)
            w[i] = 1LL * w[i - 1] * wn % P;
        InvL = Inv(L);
    }
    void NTT(int *F) {
        for(int i = 0;i < L; ++i)
            if(R[i] > i)
                std::swap(F[i], F[R[i]]);
        for(int i = 1, d = L >> 1; i < L; i <<= 1, d >>= 1)
            for(int j = 0;j < L; j += i << 1) {
                int *l = F + j, *r = F + i + j, *p = w, tp;
                for(int k = i; k--; ++l, ++r, p += d)
                    tp = 1LL * *r * *p % P, *r = (*l - tp) % P, *l = (*l + tp) % P;
            }
    }
    void Fill(const VI &a, int *A, int m) {
        m = std::min(m, (int)a.size());
        for(int i = 0;i < m; ++i)
            A[i] = a[i];
        for(int i = m; i < L; ++i)
            A[i] = 0;
    }
    void Fill(int *A, int *B, int m) {
        for(int i = 0;i < m; ++i)
            B[i] = A[i];
        for(int i = m; i < L; ++i)
            B[i] = 0;
    }
    VI operator * (const VI &a, const VI &b) {
        const int Lim = 3000;
        int asz = a.size(), bsz = b.size(), m = asz + bsz - 1; 
        static VI c; c.resize(m);
        if(1LL * asz * bsz <= Lim) {
            for(int i = 0;i < m; ++i)	
                c[i] = 0;
            for(int i = 0;i < asz; ++i)
                for(int j = 0;j < bsz; ++j)
                    c[i + j] = (c[i + j] + 1LL * a[i] * b[j]) % P;
            return c;
        }
        Pre(m); static int A[N], B[N];
        Fill(a, A, asz); Fill(b, B, bsz);
        NTT(A); NTT(B);
        for(int i = 0;i < L; ++i)
            A[i] = (1LL * A[i] * B[i]) % P;
        NTT(A);
        for(int i = 0;i < m; ++i)
            c[i] = fix(1LL * A[L - i & L - 1] * InvL % P);
        return c;
    }
    VI Inv(const VI &a, int m) {
        static int A[N], B[N], C[N];
        for(int i = 0;i < m; ++i)
            A[i] = 0;
        A[0] = ::Inv(a[0]); int n = 1;
        for(;n < m;) {
            Pre(n << 2);
            Fill(A, B, n);
            Fill(a, C, n << 1);
            NTT(B); NTT(C);
            for(int i = 0;i < L; ++i)
                B[i] = 1LL * B[i] * B[i] % P * C[i] % P;
            NTT(B);
            n <<= 1;
            for(int i = 0; i < n; ++i)
                A[i] = ((A[i] << 1) - 1LL * B[L - i & L - 1] * InvL) % P;
        }
        static VI c; c.resize(m);
        for(int i = 0;i < m; ++i)
            c[i] = fix(A[i]);
        return c;
    }
    VI deri(const VI &a) {
        int n = a.size();
        if(n == 1)
            return VI(1, 0);
        static VI c; c.resize(n - 1);
        for(int i = 1;i < n; ++i)
            c[i - 1] = 1LL * a[i] * i % P;
        return c;
    }
    VI inte(const VI &a) {
        int n = a.size();
        static VI c; c.resize(n + 1);
        for(int i = 1;i <= n; ++i)
            c[i] = 1LL * a[i - 1] * ::Inv(i) % P;
        c[0] = 0; 
        return c;
    }
    VI Ln(const VI &a, int m) {
        static VI f;
        f = deri(a) * Inv(a, m - 1);
        f.resize(m - 1);
        return inte(f);
    }
    VI Exp(const VI &a, int m) {
        static VI f, g; f.resize(1); f[0] = 1;
        int n = 1, asz = a.size();
        for(;n < m;) {
            n <<= 1;
            g = Ln(f, n);
            for(int i = 0;i < n; ++i)
                g[i] = i < asz ? fix(-g[i] + a[i]) : fix(-g[i]);
            (++g[0]) %= P;
            f = f * g;
            f.resize(n);
        }
        f.resize(m);
        return f;
    }
    VI f; int ivf[N], fac[N], p, k;
    void Work() {
        if(y == 1) return printf("%d\n", Pow(n, (n - 2 << 1) % (P - 1))), void();
        fac[0] = 1;
        for(int i = 1;i <= n; ++i)
            fac[i] = 1LL * fac[i - 1] * i % P;
        ivf[n] = ::Inv(fac[n]);
        for(int i = n; i; --i)
            ivf[i - 1] = 1LL * ivf[i] * i % P;
        p = ::Inv(y) - 1;
        k = 1LL * n * n % P * ::Inv(p) % P;
        f.push_back(0);
        for(int i = 1;i <= n; ++i)
            f.push_back(1LL * k * Pow(i, i) % P * ivf[i] % P);
        f = Exp(f, n + 1);
        printf("%d\n", 1LL * Pow(1LL * p * y % P, n) * Pow(n, P - 5) % P * f[n] % P * fac[n] % P);
    }
}
int main() {
    n = ri(); y = ri(); int op = ri();
    if(!op) Solve0::Work();
    else if(op == 1) Solve1::Work();
    else  Solve2::Work();
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值