[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;
}

posted @ 2026-09-10 14:35  Czl2007  阅读(8)  评论(0)    收藏  举报