题目传送门
最近老是在肝一些神仙生成函数题。。。哎,肝败吓疯。其实luogu题解里面的那篇已经很详细了,这篇题解纯属个人整理,建议是到到luogu题解去看。
题目大意:告诉你有俩棵有标号无根树,如果某两个节点共用了某条边,那么这两个点的权值必须相同,点权范围在 [ 1 , y ] [1,y] [1,y]内,有三个任务,求在给定2,1,0棵树的情况下构造树和点权的方案数。
如果两棵树都给的话,就是把都存在的边放在图中,假设有 c n t cnt cnt个连通块,答案就是 y c n t y^{cnt} ycnt,因为是树,所以答案就是 y n − m y^{n-m} yn−m,其中 m m m是边数。
现在少了一棵树,这个时候我们就要考虑怎么形式化问题。
不难发现,由于答案只和重合的边条数有关系,所以最简单的想法是枚举重合的边集。
A n s = ∑ S F ( S ) y n − ∣ S ∣ Ans=\sum\limits_SF(S)y^{n-|S|} Ans=S∑F(S)yn−∣S∣
其中 F ( S ) F(S) F(S)表示与初始树重合边集恰好为 S S S的方案数。
看到我加粗了恰好两个字就知道我要干什么了对吗:-)
设 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)=S⊆T∑(−1)∣T∣−∣S∣C(T)
这个时候带回原式化简一波
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=S∑F(S)yn−∣T∣=S∑S⊆T∑(−1)∣T∣−∣S∣C(T)yn−∣S∣=ynT∑(−1)∣T∣C(T)S⊆T∑(−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)∣T∣C(T)∑iC∣T∣i(−y)i=ynT∑(−1)∣T∣C(T)(1−y1)∣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=ynT∑C(T)(y1−1)∣T∣
然而到目前为止,复杂度仍然是指数级的,瓶颈在于 C ( T ) C(T) C(T),因此我们要继续形式化 C ( T ) C(T) C(T)。
考虑如果我把 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,a2⋯ak,对于任意两个连通块 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(n−a1)−a1a2⋮−aka1−a1a2a2(n−a2)⋮−aka2⋯⋯⋱⋯−a1ak−a2ak⋯ak(n−ak)⎦⎥⎥⎥⎤
先去掉一行一列,得到
∣ 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(n−a1)−a2a1⋮−ak−1a1−a1a2a2(n−a2)⋮−ak−1a2⋯⋯⋱⋯−a1ak−1−a2ak−1⋯ak−1(n−ak−1)∣∣∣∣∣∣∣∣∣
发现其实每行都有一个 a i a_i ai可以提一个 ∏ i k − 1 a i \prod_i^{k-1}a_i ∏ik−1ai。得到
∣ ( 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| ∣∣∣∣∣∣∣∣∣(n−a1)−a1⋮−a1−a2(n−a2)⋮−a2⋯⋯⋱⋯−ak−1−ak−1⋯(n−ak−1)∣∣∣∣∣∣∣∣∣
发现每行的和都是 n − ∑ i k − 1 a i = a k n-\sum_i^{k-1}a_i=a_k n−∑ik−1ai=ak,因此把 2 ⋯ k − 1 2\cdots k-1 2⋯k−1列加到第 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| ∣∣∣∣∣∣∣∣∣akak⋮ak−a2(n−a2)⋮−a2⋯⋯⋱⋯−ak−1−ak−1⋯(n−ak−1)∣∣∣∣∣∣∣∣∣
这样的话,每一列除了对角线上的元素都是相同的。考虑将每行减去第一行,就得到了一个上三角了。
∣ 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| ∣∣∣∣∣∣∣∣∣ak0⋮0−a2n⋮0⋯⋯⋱⋯−ak−10⋯n∣∣∣∣∣∣∣∣∣
手玩了一阵子的行列式之后,发现答案实际上就是 C ( T ) = n k − 2 ∏ i k a i C(T)=n^{k-2}\prod_i^{k}a_i C(T)=nk−2∏ikai,其中 k k k是连通块个数, a i a_i ai是连通块大小。
因为 T T T是树上的边集,所以连通块的个数 k = n − ∣ T ∣ k=n-|T| k=n−∣T∣
进一步带入化简可以得到 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=ynT∑nn−∣T∣−2∏in−∣T∣ai(y1−1)∣T∣
为了方便起见,我们设 p = 1 y − 1 p=\frac{1}{y}-1 p=y1−1
考虑将 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=n2ynpnT∑∏in−∣T∣pnai
设 k = n p k=\frac{n}{p} k=pn,现在问题转化成了,将一棵树划分成若干个连通块,每个连通块的贡献是 k k k乘上连通块大小,一个划分的权值是所有连通块贡献之积,求所有划分权值之和。
这个问题显然可以用一个 D p Dp 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,jj→fu,i+j∗→fu,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∗′=fu′fv+fufv′+fu′f′v(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)kf′v(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. {gu∗tu∗=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
发现其实 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=ynT∑C2(T)p∣T∣=ynT∑n2n−2∣T∣−4i∏2n−2∣T∣aip∣T∣=n4ynp2ni∏2n−2∣T∣pn2ai2
我们同样令 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} ii−2中方案,每种方案的权值都是 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)=ii−2i2=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
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;
}