[SNOI2024] 公交线路 题解(观察,点减边容斥,优化trick)
过了一段时间再看这道题还是比较难出思路,记录一下做法。
题意
传送门
给定一棵 n n n 个点的无根树,考虑 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n−1) 条端点不同的树上路径。对于这些路径的一个子集 S S S,称它是好的当且仅当:
- 考虑一张新图 G G G,对于一对点 u , v u, v u,v,当且仅当存在 S S S 中的一条路径 P P P,满足 u , v u, v u,v 都在 P P P 上,会在 u , v u, v u,v 之间连一条边权为 1 1 1 的边。
- 要求 G G G 中任意两点联通并且最短距离不超过 2 2 2。
求出有多少个子集 S S S 是 好的,答案对 998244353 998244353 998244353 取模。
1 ≤ n ≤ 3000 1 \leq n \leq 3000 1≤n≤3000。
分析
若两个点在 G G G 中不连通,那么称它们的距离为 ∞ \infty ∞。我们需要保证点对之间的最大距离 ≤ 2 \leq 2 ≤2。
有以下性质:
- 点对之间的最大距离一定是由两个叶子提供。
看起来就很对,证明就是考虑最大距离的点对不是叶子,那么可以往子树中的叶子调整,这样距离一定不会减小。
对于一个叶子 x x x,设所有经过它的路径的并为 S ( x ) S(x) S(x),那么容易知道 S ( x ) S(x) S(x) 是一个联通块并且 S ( x ) S(x) S(x) 中的点到 x x x 的距离为 1 1 1(除去 x x x 外)。只需要保证任意两个叶子的 S ( x ) S(x) S(x) 交非空那么子集就是好的。
树上若干连通块两两有交,等价于所有这些连通块的交非空。并且交集也是一个连通块。
因此我们就把一个合法子集对应到了一个连通块上。那么就可以点减边容斥。下面只讨论对点的计算,对边同理。
设枚举的点为 u u u,以 u u u 为根。我们需要计算 有多少种选路径的方案使得对每个叶子都至少存在一条路径满足 u u u 和叶子都在路径上。显然可以 容斥,记 f i f_{i} fi 目前钦定了 i i i 个叶子不合法,每次背包转移即可。单次复杂度 O ( n 2 ) O(n^2) O(n2),总复杂度 O ( n 3 ) O(n^3) O(n3)。
考虑优化,注意到如果固定根,对于一个点 u u u 而言,只考虑 u u u 子树内的叶子,那么背包的总复杂度就完全等于树背包的复杂度,是 O ( n 2 ) O(n^2) O(n2) 的。然后对于 u u u 子树以外的叶子,发现在知道子树内的钦定状态下,可以直接算出这些叶子都合法的方案数。因此复杂度变成 O ( n 2 ) O(n^2) O(n2) 了。
CODE:
// key observation: 每个叶子之间相连的点集有交并且是一个连通块。
// 然后可以 点减边容斥, O(n^3) 是容易的
// 优化可以考虑对于枚举的点,对子树内的叶子容斥, 对子树外的叶子直接钦定能到这个点即可。 这是可以算的。复杂度变成了 O(n^2)
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 3010;
const int mod = 998244353;
inline int add(int x, int y) {return x + y >= mod ? x + y - mod : x + y;}
inline int del(int x, int y) {return x - y < 0 ? x - y + mod : x - y;}
inline int mul(int x, int y) {return 1LL * x * y % mod;}
int n, root, deg[N], mi[N * N], f[N][N], h[N][N], C[N][N], g[N]; // f[i][j] 表示以 i 为根的子树,当前钦定了 j 个叶子无法到达 i 的方案数
int leaf[N], sz[N], fat[N];
vector< int > E[N];
void dfs(int x, int fa) {fat[x] = fa; sz[x] = 1; leaf[x] = (deg[x] == 1);for(auto v : E[x]) {if(v == fa) continue;dfs(v, x); sz[x] += sz[v];leaf[x] += leaf[v];}
}
inline int sign(int x) {return (x & 1) ? mod - 1 : 1;}
inline void get(int x) { // 处理 x 为根的子树的答案。 f[x][0] = 1; int nsz = 1, Leaf = 0;for(auto v : E[x]) {if(v == fat[x]) continue;for(int i = 0; i <= Leaf + leaf[v]; i ++ ) g[i] = 0;for(int i = 0; i <= Leaf; i ++ ) { // 枚举刚才钦定了几个 for(int j = 0; j <= leaf[v]; j ++ ) { // 现在钦定几个 g[i + j] = add(g[i + j], mul(f[x][i], mul(mul(C[leaf[v]][j], mi[(nsz - i) * (sz[v] - j) + sz[v] * (sz[v] - 1) / 2]), sign(j))));}}for(int i = 0; i <= Leaf + leaf[v]; i ++ ) f[x][i] = g[i];nsz += sz[v]; Leaf += leaf[v];}
}
inline int calc(int x) { // 计算以 x 在交里面的方案数, 以及 x 和儿子之间的边 在交里的方案数乘容斥系数 int res = 0;if(x == root) {for(int i = 0; i <= leaf[x]; i ++ ) res = add(res, f[x][i]);}else {for(int i = 0; i <= leaf[x]; i ++ ) // 枚举已经钦定了多少个 res = add(res, mul(f[x][i], mul(mi[(n - sz[x] - (leaf[root] - leaf[x])) * (sz[x] - i) + (n - sz[x]) * (n - sz[x] - 1) / 2], h[sz[x] - i][leaf[root] - leaf[x]])));}int tmp = 0;for(auto v : E[x]) {if(v == fat[x]) continue;int p = leaf[root] - leaf[v], q = leaf[v];// 容斥一手int lst = tmp;for(int i = 0; i <= p; i ++ ) { // 有 i 个不能到 tmp = add(tmp, mul(sign(i), mul(C[p][i], mul(mi[(n - sz[v]) * (n - sz[v] - 1) / 2], mul(mi[sz[v] * (sz[v] - 1) / 2 + (sz[v] - q) * (n - sz[v] - i)], h[n - sz[v] - i][q])))));res = del(res, mul(sign(i), mul(C[p][i], mul(mi[(n - sz[v]) * (n - sz[v] - 1) / 2], mul(mi[sz[v] * (sz[v] - 1) / 2 + (sz[v] - q) * (n - sz[v] - i)], h[n - sz[v] - i][q])))));} }return res;
}
int main() {scanf("%d", &n);mi[0] = 1; for(int i = 1; i <= n * n; i ++ ) mi[i] = mul(mi[i - 1], 2);for(int i = 0; i <= n; i ++ ) {int v = del(mi[i], 1), nv = (v == 0 ? 0 : 1);for(int j = 0; j <= n; j ++ ) {h[i][j] = nv;nv = mul(nv, v);}}for(int i = 0; i <= n; i ++ )for(int j = 0; j <= i; j ++ ) {if(!j) C[i][j] = 1;else C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]); }for(int i = 1; i < n; i ++ ) {int u, v; scanf("%d%d", &u, &v);E[u].pb(v); E[v].pb(u);deg[u] ++; deg[v] ++;}for(int i = 1; i <= n; i ++ ) {if(deg[i] != 1) {root = i; break;}}dfs(root, 0);for(int i = 1; i <= n; i ++ ) get(i);int res = 0;for(int i = 1; i <= n; i ++ ) res = add(res, calc(i));cout << res << endl;return 0;
}