[SNOI2024] 公交线路 题解(观察,点减边容斥,优化trick)
过了一段时间再看这道题还是比较难出思路,记录一下做法。
题意
给定一棵 \(n\) 个点的无根树,考虑 \(\frac{n(n - 1)}{2}\) 条端点不同的树上路径。对于这些路径的一个子集 \(S\),称它是好的当且仅当:
- 考虑一张新图 \(G\),对于一对点 \(u, v\),当且仅当存在 \(S\) 中的一条路径 \(P\),满足 \(u, v\) 都在 \(P\) 上,会在 \(u, v\) 之间连一条边权为 \(1\) 的边。
- 要求 \(G\) 中任意两点联通并且最短距离不超过 \(2\)。
求出有多少个子集 \(S\) 是 好的,答案对 \(998244353\) 取模。
\(1 \leq n \leq 3000\)。
分析
若两个点在 \(G\) 中不连通,那么称它们的距离为 \(\infty\)。我们需要保证点对之间的最大距离 \(\leq 2\)。
有以下性质:
- 点对之间的最大距离一定是由两个叶子提供。
看起来就很对,证明就是考虑最大距离的点对不是叶子,那么可以往子树中的叶子调整,这样距离一定不会减小。
对于一个叶子 \(x\),设所有经过它的路径的并为 \(S(x)\),那么容易知道 \(S(x)\) 是一个联通块并且 \(S(x)\) 中的点到 \(x\) 的距离为 \(1\)(除去 \(x\) 外)。只需要保证任意两个叶子的 \(S(x)\) 交非空那么子集就是好的。
树上若干连通块两两有交,等价于所有这些连通块的交非空。并且交集也是一个连通块。
因此我们就把一个合法子集对应到了一个连通块上。那么就可以点减边容斥。下面只讨论对点的计算,对边同理。
设枚举的点为 \(u\),以 \(u\) 为根。我们需要计算 有多少种选路径的方案使得对每个叶子都至少存在一条路径满足 \(u\) 和叶子都在路径上。显然可以 容斥,记 \(f_{i}\) 目前钦定了 \(i\) 个叶子不合法,每次背包转移即可。单次复杂度 \(O(n^2)\),总复杂度 \(O(n^3)\)。
考虑优化,注意到如果固定根,对于一个点 \(u\) 而言,只考虑 \(u\) 子树内的叶子,那么背包的总复杂度就完全等于树背包的复杂度,是 \(O(n^2)\) 的。然后对于\(u\) 子树以外的叶子,发现在知道子树内的钦定状态下,可以直接算出这些叶子都合法的方案数。因此复杂度变成 \(O(n^2)\) 了。
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;
}

浙公网安备 33010602011771号