• 【LOJ #2983】「WC2019」数树


    Description

    题目链接:

    本题包含三个问题:

    • 问题 0:已知两棵 (n) 个节点的树的形态(两棵树的节点标号均为 (1)(n)),其中第一棵树是红树,第二棵树是蓝树。要给予每个节点一个 ([1, y]) 中的整数,使得对于任意两个节点 (p, q),如果存在一条路径 ((a_1 = p, a_2, cdots , a_m = q)) 同时属于这两棵树,则 (p, q) 必须被给予相同的数。求给予数的方案数。
    • 问题 1:已知蓝树,对于红树的所有 (n^{n−2}) 种选择方案,求问题 0 的答案之和。
    • 问题 2:对于蓝树的所有 (n^{n−2}) 种选择方案,求问题 1 的答案之和。

    输出答案对 (998, 244, 353) 取模的结果。

    (n leq 10^5)

    时空限制:( exttt{4s/512MB})

    Solution

    注意 (y=1) 的情况会带来一些困扰,提前判掉即可。

    问题 0

    根据题意,由两棵树边集的交集构成的同一个连通块内的点均要取同一个颜色,森林的连通块数等于点数减边数,因此求出边集交集的大小 (k),答案就是 (y^{n-k})

    可以用 ( ext{std::map}) 实现这个过程。

    问题 1

    暴力矩阵树

    假设每个生成树的权值为 (y^{n-k})(k) 表示这棵生成树和给定的树的边集交集大小,那么要求的就是所有生成树的权值之和。不妨将 (y^n) 提取出来,那么每条原树的边的边权都是 (y^{-1}),不是原树的边的边权就是 (1)

    直接套用矩阵树定理即可。时间复杂度 (mathcal O(n^3))

    听说这个矩阵比较有性质可以优化到 (mathcal O(n)),因为我太菜无法理解题解的做法,所以这种做法就到此为止了。

    推式子

    不妨将第一棵树的边集叫做 (E_1),第二棵树的边集叫做 (E_2),那么就有下面这个式子成立

    [egin{aligned} mathrm{ans}&=sum_{E_2}y^{n-|E_1cap E_2|} \ &=sum_{Ssubseteq E_1}y^{n-|S|}sum_{E_2}[S=E_1cap E_2] end{aligned} ]

    不妨设

    [f(S)=sum_{E_2}[S=E_1cap E_2] ]

    那么 (f(S)) 显然不太好求,不妨考虑一下 (f(S)) 的实际含义,(f(S)) 就表示将强制 (E_2) 首先要选边集 (S) 的边,并且强制 (E_2) 不能选 (E_1setminus S) 的边。

    前者比较好处理,但是后者不太好处理。我们考虑先去掉后面那个条件,就想到设

    [g(S)=sum_{E_2}[Ssubseteq E_1cap E_2] ]

    显然有

    [g(S)=sum_{Ssubseteq T}f(T) ]

    容斥一下就是

    [f(S)=sum_{Ssubseteq T}(-1)^{|T|-|S|}g(T) ]

    所以

    [egin{aligned} mathrm{ans}&=sum_{Ssubseteq E_1}y^{n-|S|}f(S)\ &=sum_{Ssubseteq E_1}y^{n-|S|}sum_{Ssubseteq Tsubseteq E_1}(-1)^{|T|-|S|}g(T)\ &=sum_{Tsubseteq E_1}g(T)sum_{Ssubseteq T}y^{n-|S|}(-1)^{|T|-|S|}\ &=y^nsum_{Tsubseteq E_1}g(T)sum_{i=0}^{|T|}inom{|T|}{i}y^{-i}(-1)^{|T|-i}\ &=y^nsum_{Tsubseteq E_1}g(T)(y^{-1}-1)^{|T|} end{aligned} ]

    如何求 (g(T))

    现在先考虑 (g(T)) 是怎么求的,从我们为什么要定义 (g(T)) 那里我们已经知道了 (g(T)) 的具体含义:强制 (E_2) 一定要选 (T) 的边,(E_2) 的方案数。

    这相当于,我们已经确定了一棵有标号无根树中的若干条边,考虑剩下的边的选取方案。不妨考虑这个边集 (T),已经将当前的点集并成 (k) 个连通块,第 (i) 个连通块的大小为 (a_i)。我们可以将每个连通块看成一个点,那么每条连接两个连通块 (a_i,a_j) 之间的边都可以有 (a_icdot a_j) 种选择。接下去的推导就可以用 prufer 序列或者矩阵树定理了。

    prufer 序列推导

    (p_1,p_2,cdots,p_{k-2}) 表示 prufer 序列,(q_i) 表示 (i) 在 prufer 序列中的出现次数。因为每个点的度数为 (q_i+1),那么第 (i) 个连通块的贡献就是 (a_i^{q_i+1})。具体地

    [egin{aligned} g(T)&=sum_{{p_{k-2}}}prod_{i=1}^ka_i^{q_i+1}\ &=prod_{i=1}^ka_isum_{{p_{k-2}}}prod_{i=1}^ka_i^{q_i}\ &=prod_{i=1}^ka_isum_{{p_{k-2}}}prod_{i=1}^{k-2}a_{p_i}\ &=prod_{i=1}^ka_iprod_{i=1}^{k-2}sum_{j=1}^ka_{j}\ &=n^{k-2}prod_{i=1}^ka_i end{aligned} ]

    矩阵树定理推导

    去掉最后一行最后一列的基尔霍夫矩阵的行列式是

    [egin{vmatrix} a_1(n-a_1) & -a_1a_2 & cdots & -a_1a_{k-1} \ -a_1a_2 & a_2(n-a_2) & cdots & -a_2a_{k-1} \ cdots & cdots & cdots & cdots \ -a_1a_{k-1}& -a_2a_{k-1}& cdots & a_{k-1}(n-a_{k-1}) end{vmatrix} ]

    把每一行的 (a_i) 提出来就是

    [prod_{i=1}^{k-1}a_i egin{vmatrix} n-a_1 & -a_2 & cdots & -a_{k-1} \ -a_1 & n-a_2 & cdots & -a_{k-1} \ cdots & cdots & cdots & cdots \ -a_1 & -a_2 & cdots & n-a_{k-1} end{vmatrix} ]

    前面 (k-2) 行全部减去最后一行

    [prod_{i=1}^{k-1}a_i egin{vmatrix} n & 0 & cdots & 0 & -n \ 0 & n & cdots & 0 & -n\ cdots & cdots & cdots & cdots & cdots \ 0 & 0 & cdots & n & -n \ -a_1 & -a_2& cdots & -a_{k-2} & n-a_{k-1} end{vmatrix} ]

    前面 (k-2) 列全部加到最后一列上

    [prod_{i=1}^{k-1}a_i egin{vmatrix} n & 0 & cdots & 0 & 0 \ 0 & n & cdots & 0 & 0\ cdots & cdots & cdots & cdots & cdots \ 0 & 0 & cdots & n & 0 \ -a_1 & -a_2& cdots & -a_{k-2} & a_k end{vmatrix} ]

    于是

    [g(T)=prod_{i=1}^{k-1}a_i egin{vmatrix} n & 0 & cdots & 0 & 0 \ 0 & n & cdots & 0 & 0\ cdots & cdots & cdots & cdots & cdots \ 0 & 0 & cdots & n & 0 \ -a_1 & -a_2& cdots & -a_{k-2} & a_k end{vmatrix}=n^{k-2}prod_{i=1}^{k}a_i ]

    继续推式子

    推导出这个以后我们继续考虑推式子。我们设 (R(T)) 表示固定了边集 (T) 后得到的所有连通块的大小之积。

    [egin{aligned} mathrm{ans}&=y^nsum_{Tsubseteq E_1}g(T)(y^{-1}-1)^{|T|}\ &=y^nsum_{Tsubseteq E_1}n^{n-|T|-2}R(T)(y^{-1}-1)^{|T|}\ &=frac{y^nn^n}{n^2}sum_{Tsubseteq E_1}R(T)left(frac{y^{-1}-1}{n} ight)^{|T|}\ end{aligned} ]

    我们不妨设 (a(T)_i) 表示 (T) 得到的第 (i) 个连通块大小,那么有

    [egin{aligned} mathrm{ans}&=frac{y^nn^n}{n^2}sum_{Tsubseteq E_1}left(frac{y^{-1}-1}{n} ight)^{|T|}prod_{i=1}^{n-|T|}a(T)_i\ &=frac{(1-y)^n}{n^2}sum_{Tsubseteq E_1}prod_{i=1}^{n-|T|}a(T)_ileft(frac{n}{y^{-1}-1} ight) end{aligned} ]

    树形 DP

    不妨设 (K=frac{n}{y^{-1}-1}),现在只需要考虑计算

    [sum_{Tsubseteq E_1}prod_{i=1}^{n-|T|}Kcdot a(T)_i ]

    我们发现边集与每种划分连通块的方案形成双射。现在可以考虑将枚举边集看成划分连通块。

    相当于对于每种划分连通块的方案,每个大小为 (s) 的连通块的权值是 (Ks),每个方案的权值是所有连通块的权值之积,对所有方案的权值求和。这个显然是可以把当前点的连通块大小记到状态里 (mathcal O(n^2)) 树形 DP 做的,但是不足以通过本题。

    考虑这个式子的组合意义,相当于将整棵树划分成若干连通块,然后在每个连通块内选一个点,并乘上 (K) 的权值。那么考虑到这里我们就可以 (mathcal O(n)) 树形 DP 了。

    我们设 (f(u,0/1)) 表示考虑到 (u) 的子树,并且 (u) 所在的连通块是否选择了点的所有方案权值之和。

    转移可以看代码。

    inline void dfs(int u, int pre)
    {
        f[u][0] = 1; 
        f[u][1] = K; 
    
        foredge(u)
            if (v != pre)
            {
                dfs(v, u);
                int f0 = (1LL * f[u][0] * f[v][0] + 1LL * f[u][0] * f[v][1]) % mod; 
                int f1 = (1LL * f[u][1] * f[v][1] + 1LL * f[u][1] * f[v][0]) % mod; 
                add(f1, 1LL * f[u][0] * f[v][1] % mod); 
    
                f[u][0] = f0; 
                f[u][1] = f1; 
            }
    }
    

    问题 2

    推式子是类似的,不过这时候就没有限制 (Tsubseteq E_1) 了,(T) 可以选取任意无环无重边边集。

    [mathrm{ans}=y^nsum_{T}g(T)^2(y^{-1}-1)^{|T|} ]

    代入 (g(T)) 的表达式我们有

    [egin{aligned} mathrm{ans}&=(1-y)^nsum_{T}R(T)^2n^{2(n-|T|-2)}left(frac{1}{y^{-1}-1} ight)^{n-|T|}\ &=frac{(1-y)^n}{n^4}sum_{T}R(T)^2left(frac{n^2}{y^{-1}-1} ight)^{n-|T|}\ &=frac{(1-y)^n}{n^4}sum_{T}prod_{i=1}^{n-|T|}a(T)_i^2left(frac{n^2}{y^{-1}-1} ight)\ end{aligned} ]

    与问题 1 不同的地方体现在,问题 2 没有给出一棵树,因此每种连通块划分方案对应着多个边集,需要在每个连通块内部任意连一棵树出来。而且有标号使得划分连通块大小后还需要给每个连通块分配标号。

    因此这涉及到有标号组合对象的拼接,我们考虑指数生成函数,设 (F(x)) 表示每个连通块的指数生成函数,那么

    [egin{aligned} F(x)&=sum_{i=1}^{+infty}frac{i^2cdot i^{i-2}cdot n^2}{y^{-1}-1} cdotfrac{x^i}{i!}\ &=sum_{i=1}^{+infty}frac{i^icdot n^2}{y^{-1}-1} cdotfrac{x^i}{i!} end{aligned} ]

    那么就有

    [egin{aligned} mathrm{ans}&=frac{(1-y)^nn!}{n^4}[x^n]sum_{k=1}^nfrac{F(x)^k}{k!}\ &=frac{(1-y)^nn!}{n^4}[x^n]e^{F(x)} end{aligned} ]

    需要除以 (k!) 是因为连通块是无标号的,然后套个多项式 exp 的板子就行了。

    时间复杂度 (mathcal O(n log n))

    #include <bits/stdc++.h>
    
    template <class T>
    inline void read(T &x)
    {
    	static char ch; 
    	while (!isdigit(ch = getchar())); 
    	x = ch - '0'; 
    	while (isdigit(ch = getchar()))
    		x = x * 10 + ch - '0'; 
    }
    
    typedef long long s64; 
    
    const int mod = 998244353; 
    
    inline void add(int &x, const int &y)
    {
    	x += y; 
    	if (x >= mod)
    		x -= mod; 
    }
    
    inline void dec(int &x, const int &y)
    {
    	x -= y; 
    	if (x < 0)
    		x += mod; 
    }
    
    inline int qpow(int x, int y)
    {
    	int res = 1; 
    	for (; y; y >>= 1, x = 1LL * x * x % mod)
    		if (y & 1)
    			res = 1LL * res * x % mod; 
    	return res; 
    }
    
    const int MaxN = 1e6 + 5; 
    
    int n, y, opt; 
    
    namespace task0
    {
    	std::map<int, bool> bo[MaxN]; 
    	inline void answer()
    	{
    		for (int i = 1; i < n; ++i)
    		{
    			int u, v; 
    			read(u), read(v); 
    			if (u > v)
    				std::swap(u, v); 
    			bo[u][v] = true; 
    		}
    
    		int res = n; 
    		for (int i = 1; i < n; ++i)
    		{
    			int u, v; 
    			read(u), read(v); 
    			if (u > v)
    				std::swap(u, v); 
    			res -= bo[u][v]; 
    		}
    		std::cout << qpow(y, res) << '
    '; 
    	}
    }
    
    namespace task1
    {
    	int K; 
    	int f[MaxN][2]; 
    
    	int ect, adj[MaxN]; 
    	int nxt[MaxN << 1], to[MaxN << 1]; 
    
    	#define foredge(u) for (int e = adj[u], v; v = to[e], e; e = nxt[e])
    
    	inline void addEdge(int u, int v)
    	{
    		nxt[++ect] = adj[u]; 
    		adj[u] = ect; 
    		to[ect] = v; 
    	}
    
    	inline void dfs(int u, int pre)
    	{
    		f[u][0] = 1; 
    		f[u][1] = K; 
    
    		foredge(u)
    			if (v != pre)
    			{
    				dfs(v, u);
    				int f0 = (1LL * f[u][0] * f[v][0] + 1LL * f[u][0] * f[v][1]) % mod; 
    				int f1 = (1LL * f[u][1] * f[v][1] + 1LL * f[u][1] * f[v][0]) % mod; 
    				add(f1, 1LL * f[u][0] * f[v][1] % mod); 
    
    				f[u][0] = f0; 
    				f[u][1] = f1; 
    			}
    	}
    
    	inline void answer()
    	{
    		for (int i = 1; i < n; ++i)
    		{
    			int u, v; 
    			read(u), read(v); 
    			addEdge(u, v); 
    			addEdge(v, u); 
    		}
    
    		K = 1LL * n * qpow(qpow(y, mod - 2) - 1, mod - 2) % mod; 
    
    		dfs(1, 0); 
    
    		int d = 1LL * qpow(mod - y + 1, n) * qpow(1LL * n * n % mod, mod - 2) % mod; 
    		std::cout << 1LL * f[1][1] * d % mod << '
    '; 
    	}
    }
    
    namespace polynomial
    {
    	int inv[MaxN]; 
    	int rev[MaxN], P, L; 
    
    	inline void inv_init()
    	{
    		inv[1] = 1; 
    		for (int i = 2; i < MaxN; ++i)
    			inv[i] = 1LL * inv[mod % i] * (mod - mod / i) % mod; 
    	}
    	inline void DFT_init(int n)
    	{
    		P = 0, L = 1; 
    		while (L < n)
    		{
    			L <<= 1; 
    			++P; 
    		}
    		for (int i = 1; i < L; ++i)
    			rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (P - 1)); 
    	}
    	inline void resize(int *a, int cur_n, int n)
    	{
    		for (int i = n; i < cur_n; ++i)
    			a[i] = 0; 
    	}
    
    	inline void DFT(int *a, int n, int opt)
    	{
    		for (int i = 0; i < n; ++i)
    			if (i < rev[i])
    				std::swap(a[i], a[rev[i]]); 
    
    		int g = opt == 1 ? 3 : (mod + 1) / 3;
    		for (int k = 1; k < n; k <<= 1)
    		{
    			int omega = qpow(g, (mod - 1) / (k << 1)); 
    			for (int i = 0; i < n; i += k << 1)
    			{
    				int x = 1; 
    				for (int j = 0; j < k; ++j)
    				{
    					int u = a[i + j]; 
    					int v = 1LL * x * a[i + j + k] % mod; 
    					add(a[i + j] = u, v); 
    					dec(a[i + j + k] = u, v); 
    					x = 1LL * x * omega % mod; 
    				}
    			}
    		}
    
    		if (opt == -1)
    		{
    			for (int i = 0; i < n; ++i)
    				a[i] = 1LL * a[i] * inv[n] % mod; 
    		}
    	}
    
    	inline void dot_mul(int *a, int *b, int *c)
    	{
    		for (int i = 0; i < L; ++i)
    			c[i] = 1LL * a[i] * b[i] % mod; 
    	}
    	inline void mul(int *a, int *b, int *c, int n, int m)
    	{
    		DFT_init(n + m - 1); 
    		static int ta[MaxN], tb[MaxN]; 
    		for (int i = 0; i < L; ++i) ta[i] = tb[i] = 0; 
    		for (int i = 0; i < n; ++i) ta[i] = a[i]; 
    		for (int i = 0; i < m; ++i) tb[i] = b[i]; 
    		DFT(ta, L, 1), DFT(tb, L, 1); 
    		dot_mul(ta, tb, c), DFT(c, L, -1), resize(c, L, n + m - 1); 
    	}
    	inline void inverse(int *a, int *b, int n)
    	{
    		static int ta[MaxN], tb[MaxN], m; 
    		for (int i = 0; i < (n << 2); ++i)
    			ta[i] = tb[i] = b[i] = 0; 
    
    		b[0] = qpow(a[0], mod - 2); 
    		for (m = 1; m < n; m <<= 1)
    		{
    			DFT_init(m << 2); 
    
    			for (int i = 0; i < (m << 1); ++i)
    				ta[i] = a[i], tb[i] = b[i]; 
    			DFT(ta, L, 1), DFT(tb, L, 1); 
    			for (int i = 0; i < L; ++i)
    			{
    				int d = 1LL * ta[i] * tb[i] % mod; 
    				b[i] = 1LL * tb[i] * (2 + mod - d) % mod; 
    			}
    			DFT(b, L, -1); 
    			resize(b, L, m << 1); 
    		}
    		resize(b, m, n); 
    	}
    
    	inline void derivative(int *a, int *b, int n)
    	{
    		for (int i = 0; i < n - 1; ++i)
    			b[i] = 1LL * (i + 1) * a[i + 1] % mod; 
    		b[n - 1] = 0; 
    	}
    	inline void anti_derivative(int *a, int *b, int n)
    	{
    		for (int i = n; i >= 1; --i)
    			b[i] = 1LL * inv[i] * a[i - 1] % mod; 
    		b[0] = 0; 
    	}
    	inline void ln(int *a, int *b, int n)
    	{
    		static int ta[MaxN], tb[MaxN]; 
    		derivative(a, ta, n); 
    		inverse(a, tb, n); 
    		mul(tb, ta, b, n, n - 1); 
    		anti_derivative(b, b, n), resize(b, L, n); 
    	}
    	inline void exp(int *a, int *b, int n)
    	{
    		static int tb[MaxN], m; 
    		for (int i = 0; i < (n << 2); ++i)
    			tb[i] = b[i] = 0; 
    		b[0] = 1; 
    		for (m = 1; m < n; m <<= 1)
    		{
    			ln(b, tb, m << 1);
    
    			tb[0] = (a[0] + mod + 1 - tb[0]) % mod; 
    			for (int i = 1; i < (m << 1); ++i)
    				tb[i] = (mod - tb[i] + a[i]) % mod; 
    			mul(b, tb, b, m, m << 1); 
    			resize(b, L, m << 1); 
    		}
    		resize(b, m, n); 
    	}
    }
    
    namespace task2
    {
    	int fac[MaxN], fac_inv[MaxN]; 
    
    	inline void fac_init(int n)
    	{
    		fac[0] = 1;
    		for (int i = 1; i <= n; ++i)
    			fac[i] = 1LL * fac[i - 1] * i % mod; 
    		fac_inv[n] = qpow(fac[n], mod - 2); 
    		for (int i = n - 1; i >= 0; --i)
    			fac_inv[i] = 1LL * fac_inv[i + 1] * (i + 1) % mod; 
    	}
    	inline void answer()
    	{
    		polynomial::inv_init(); 
    		fac_init(n); 
    
    		static int f[MaxN], g[MaxN]; 
    
    		int d = 1LL * n * n % mod * qpow(qpow(y, mod - 2) - 1, mod - 2) % mod; 
    		for (int i = 1; i <= n; ++i)
    			f[i] = 1LL * d * qpow(i, i) % mod * fac_inv[i] % mod; 
    		polynomial::exp(f, g, n + 1); 
    
    		int t = 1LL * fac[n] * qpow(mod - y + 1, n) % mod;
    		t = 1LL * t * qpow(n, (4LL * (mod - 2)) % (mod - 1)) % mod; 
    
    		std::cout << 1LL * t * g[n] % mod << '
    '; 
    	}
    }
    
    int main()
    {
    	read(n), read(y), read(opt); 
    
    	if (opt == 0)
    		task0::answer(); 
    	else
    	{
    		if (y == 1)
    			std::cout << qpow(n, opt * (n - 2)) << '
    '; 
    		else
    		{
    			if (opt == 1)
    				task1::answer(); 
    			else
    				task2::answer(); 
    		}
    	}
    	return 0; 
    }
    
  • 相关阅读:
    jfreeChart柱状图各属性详细设置
    eclipse 常用快捷键及调试方法
    Java利用Preferences设置个人偏好
    图说设计模式(UML和设计模式)
    JFreeChart 使用一 饼图之高级特性
    使用批处理创建永久生效的环境变量
    tnsnames.ora 监听配置文件详解
    oracle本机登录不上dba的权限不足错误
    Oracle自动备份脚本(网上找到的资料)
    初探Docker
  • 原文地址:https://www.cnblogs.com/cyx0406/p/12263899.html
Copyright © 2020-2023  润新知