【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\)
时空限制:\(\texttt{4s/512MB}\)
Solution
注意 \(y=1\) 的情况会带来一些困扰,提前判掉即可。
问题 0
根据题意,由两棵树边集的交集构成的同一个连通块内的点均要取同一个颜色,森林的连通块数等于点数减边数,因此求出边集交集的大小 \(k\),答案就是 \(y^{n-k}\)。
可以用 \(\text{std::map}\) 实现这个过程。
问题 1
暴力矩阵树
假设每个生成树的权值为 \(y^{n-k}\),\(k\) 表示这棵生成树和给定的树的边集交集大小,那么要求的就是所有生成树的权值之和。不妨将 \(y^n\) 提取出来,那么每条原树的边的边权都是 \(y^{-1}\),不是原树的边的边权就是 \(1\)。
直接套用矩阵树定理即可。时间复杂度 \(\mathcal O(n^3)\)。
听说这个矩阵比较有性质可以优化到 \(\mathcal O(n)\),因为我太菜无法理解题解的做法,所以这种做法就到此为止了。
推式子
不妨将第一棵树的边集叫做 \(E_1\),第二棵树的边集叫做 \(E_2\),那么就有下面这个式子成立
不妨设
那么 \(f(S)\) 显然不太好求,不妨考虑一下 \(f(S)\) 的实际含义,\(f(S)\) 就表示将强制 \(E_2\) 首先要选边集 \(S\) 的边,并且强制 \(E_2\) 不能选 \(E_1\setminus S\) 的边。
前者比较好处理,但是后者不太好处理。我们考虑先去掉后面那个条件,就想到设
显然有
容斥一下就是
所以
如何求 \(g(T)\)
现在先考虑 \(g(T)\) 是怎么求的,从我们为什么要定义 \(g(T)\) 那里我们已经知道了 \(g(T)\) 的具体含义:强制 \(E_2\) 一定要选 \(T\) 的边,\(E_2\) 的方案数。
这相当于,我们已经确定了一棵有标号无根树中的若干条边,考虑剩下的边的选取方案。不妨考虑这个边集 \(T\),已经将当前的点集并成 \(k\) 个连通块,第 \(i\) 个连通块的大小为 \(a_i\)。我们可以将每个连通块看成一个点,那么每条连接两个连通块 \(a_i,a_j\) 之间的边都可以有 \(a_i\cdot 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}\)。具体地
矩阵树定理推导
去掉最后一行最后一列的基尔霍夫矩阵的行列式是
把每一行的 \(a_i\) 提出来就是
前面 \(k-2\) 行全部减去最后一行
前面 \(k-2\) 列全部加到最后一列上
于是
继续推式子
推导出这个以后我们继续考虑推式子。我们设 \(R(T)\) 表示固定了边集 \(T\) 后得到的所有连通块的大小之积。
我们不妨设 \(a(T)_i\) 表示 \(T\) 得到的第 \(i\) 个连通块大小,那么有
树形 DP
不妨设 \(K=\frac{n}{y^{-1}-1}\),现在只需要考虑计算
我们发现边集与每种划分连通块的方案形成双射。现在可以考虑将枚举边集看成划分连通块。
相当于对于每种划分连通块的方案,每个大小为 \(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
推式子是类似的,不过这时候就没有限制 \(T\subseteq E_1\) 了,\(T\) 可以选取任意无环无重边边集。
代入 \(g(T)\) 的表达式我们有
与问题 1 不同的地方体现在,问题 2 没有给出一棵树,因此每种连通块划分方案对应着多个边集,需要在每个连通块内部任意连一棵树出来。而且有标号使得划分连通块大小后还需要给每个连通块分配标号。
因此这涉及到有标号组合对象的拼接,我们考虑指数生成函数,设 \(F(x)\) 表示每个连通块的指数生成函数,那么
那么就有
需要除以 \(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) << '\n';
}
}
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 << '\n';
}
}
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 << '\n';
}
}
int main()
{
read(n), read(y), read(opt);
if (opt == 0)
task0::answer();
else
{
if (y == 1)
std::cout << qpow(n, opt * (n - 2)) << '\n';
else
{
if (opt == 1)
task1::answer();
else
task2::answer();
}
}
return 0;
}