P9167 [省选联考 2023] 城市建造
原题等价于计算有多少个点集 \(V\),满足删去 \(V\) 的导出子图中的边后,原图形成了 \(|V|\) 个连通块,且连通块大小的极差 \(\le k\)。形成 \(|V|\) 个连通块又等价于 \(V\) 中的每个点都分属不同的连通块,我们称这样的 \(V\) 为合法的。
考虑在同一个点双连通分量中的三点 \(u, v, w\)。若 \(u, v \in V\) 而 \(w \not \in V\),根据点双连通的性质,我们可以找到一条从 \(u\) 到 \(v\) 经过 \(w\) 的简单路径,此时必有两个 \(V\) 中的点属于同一个连通块。因此对于合法的 \(V\),同一个点双连通分量中的点,要么全在 \(V\) 中,要么只有一个在 \(V\) 中,要么全不在 \(V\) 中。
我们还可以发现,若 \(V\) 的导出子图不连通,则必有两个 \(V\) 中的点属于同一个连通块。因此对于合法的 \(V\),其导出子图必须连通。
上面两条性质启发我们建出原图的圆方树。不难发现,我们选择的 \(V\) 一定是在圆方树上“连通”的一些点双连通分量。具体地,用一个方点在 \(S\) 中代表该点双连通分量中的所有点都在 \(V\) 中,合法的 \(V\) 对应的 \(S\) 需要满足:若方点 \(u, v \in S\),则所有在圆方树上 \(u, v\) 间唯一简单路径上的方点都在 \(S\) 中。我们称这样的 \(S\) 为合法的。
在原图中删去 \(V\) 的导出子图中的边、在圆方树中删去 \(S\) 中的点后,原图的每个连通块和圆方树的每个连通块一一对应。定义圆方树上一个连通块的大小为连通块的圆点数,问题转化为计算有多少个圆方树上的合法方点点集 \(S\),满足删去 \(S\) 中的点后连通块大小的极差 \(\le k\)。我们称这样的 \(S\) 为符合题意的。
考虑 \(k \le 1\) 有什么用。不难猜想 \(S\) 一定包含重心,因为连通块的划分看起来十分平均。
证明:
令圆点的点权为 \(1\),方点的点权为 \(0\),找出圆方树的带权重心 \(rt\)。\(rt\) 的所有儿子子树内的点权和都 \(\le \frac{n}{2}\)。
如果 \(rt\) 为方点,则符合题意的 \(S\) 必然包含 \(rt\),因为若不包含则 \(rt\) 所在的连通块点权和 \(\ge \frac{n}{2} + 1\),而 \(rt\) 不在的连通块点权和都 \(\le \frac{n}{2} - 1\),无论 \(k\) 取多少都不符合题意。
如果 \(rt\) 为圆点,对于符合题意的 \(S\),若 \(u \in S\),则以 \(rt\) 为根时 \(u\) 的所有祖先方点都应属于 \(S\),证明同理。
证毕。
当 \(rt\) 为方点时以 \(rt\) 的任意相邻圆点为根,当 \(rt\) 为圆点时以 \(rt\) 为根,将圆方树变为有根树。这样,对于符合题意的 \(S\),若 \(u \in S\),则 \(u\) 的所有祖先方点都应属于 \(S\)。
先考虑 \(k = 0\) 时怎么做。
枚举连通块大小 \(x\),记 \(sz_i\) 表示圆方树定根后 \(i\) 的子树内的权值和。对于方点 \(u\),可以发现其是否属于 \(S\) 可以直接通过 \(sz_u\) 确定。若 \(sz_u < x\) 则 \(u \not \in S\)。若 \(sz_u > x\) 则 \(u \in S\)。若 \(sz_u = x\),如果定根后 \(u\) 的儿子数 \(\ge 2\) 则 \(u \not \in S\),否则 \(u \in S\)。证明考虑反证,若不满足上述任意条件则 \(S\) 必然不符合题意,读者可自证。
因此可以直接从小到大枚举 \(x\),用并查集维护连通性,再开一个桶 \(cnt_i\) 记录连通块大小为 \(i\) 的连通块数量即可。
再考虑 \(k = 1\)。
容易想到一个容斥,枚举连通块大小的集合 \(\{x, x + 1\}\),钦定所有连通块大小属于该集合,计算方案数,然后减去连通块大小全为 \(x\) 或全为 \(x + 1\) 的方案数。
类似 \(k = 0\),若 \(sz_u < x\) 则 \(u \not \in S\)。若 \(sz_u > x\) 则 \(u \in S\)。若 \(sz_u = x\),如果定根后 \(u\) 的儿子数 \(\ge 2\) 则 \(u \not \in S\),否则特殊处理。
特殊处理是 \(k = 1\) 的一个小难点。
先考虑 \(x > 1\) 的情况。
记 \(fa_i\) 为定根后点 \(i\) 的父亲。不难发现,对于需要特殊处理的 \(u\),若 \(fa_u\) 有若干个需要特殊处理的儿子,其中只能有至多一个 \(\not \in S\)。
我们先钦定所有需要特殊处理的点属于 \(S\),此时若 \(fa_u\) 所在连通块大小 \(> 1\),则其所有需要特殊处理的儿子只能 \(\in S\)。否则,\(fa_u\) 应有恰好一个儿子 \(\not \in S\)。
即对于所有存在需要特殊处理的儿子、且当前所在连通块大小 \(= 1\) 的 \(v\),给答案乘上 点 \(v\) 需要特殊处理的儿子数量。
但当 \(x = 1\) 时,我们应对于满足上述条件的 \(v\),给答案乘上 点 \(v\) 需要特殊处理的儿子数量加一。不难发现若 \(fa_v\) 存在则必有 \(fa_v \in S\),因此此时点 \(v\) 需要特殊处理的儿子可以都 \(\in S\)。
同样这个过程可以用并查集加桶维护。
总时间复杂度 \(O(n \alpha(n))\),瓶颈在并查集,但是精细实现可以不用并查集。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
template <typename T>
void chkmax(T &x, const T &y) {
if (x < y) x = y;
}
template <typename T>
void chkmin(T &x, const T &y) {
if (y < x) x = y;
}
constexpr int MAXN = 1e5 + 10, MOD = 998244353, INF = 0x3f3f3f3f;
void inc(int &x, int y) { x += y, x >= MOD && (x -= MOD); }
void dec(int &x, int y) { x -= y, x < 0 && (x += MOD); }
int n, k, dfn[MAXN], low[MAXN], dfc, stk[MAXN], tp, num, sz[MAXN * 2];
int mini, rt, ans1[MAXN], ans2[MAXN], fa[MAXN * 2], rec[MAXN];
bool vis[MAXN * 2];
vector<int> og[MAXN], g[MAXN * 2], buc[MAXN];
namespace dsu {
int fa[MAXN], sz[MAXN], cnt[MAXN];
void init() {
iota(fa + 1, fa + n + 1, 1);
fill(sz + 1, sz + n + 1, 1);
cnt[1] = n;
}
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) {
x = find(x);
y = find(y);
--cnt[sz[x]];
--cnt[sz[y]];
fa[x] = y;
sz[y] += sz[x];
++cnt[sz[y]];
}
} // namespace dsu
void tarjan(int u) {
dfn[u] = low[u] = ++dfc;
stk[++tp] = u;
for (auto v : og[u]) {
if (!dfn[v]) {
tarjan(v);
chkmin(low[u], low[v]);
if (low[v] == dfn[u]) {
g[u].emplace_back(++num);
g[num].emplace_back(u);
while (true) {
int t = stk[tp--];
g[num].emplace_back(t);
g[t].emplace_back(num);
if (t == v) break;
}
}
} else {
chkmin(low[u], dfn[v]);
}
}
}
void get_root(int u, int pre) {
sz[u] = (u <= n);
int tmp = 0;
for (auto v : g[u]) {
if (v == pre) continue;
get_root(v, u);
sz[u] += sz[v];
chkmax(tmp, sz[v]);
}
chkmax(tmp, n - sz[u]);
if (tmp < mini) {
mini = tmp;
rt = u;
}
}
void dfs(int u, int pre) {
fa[u] = pre;
sz[u] = (u <= n);
for (auto v : g[u]) {
if (v == pre) continue;
dfs(v, u);
sz[u] += sz[v];
}
if (u > n) buc[sz[u]].emplace_back(u);
}
int main() {
freopen("cities.in", "r", stdin);
freopen("cities.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
int m;
cin >> n >> m >> k;
while (m--) {
int u, v;
cin >> u >> v;
og[u].emplace_back(v);
og[v].emplace_back(u);
}
num = n;
tarjan(1);
mini = INF;
get_root(1, 0);
if (rt > n) rt = g[rt][0];
dfs(rt, 0);
dsu::init();
for (int i = 1; i < n; ++i) {
for (auto u : buc[i]) {
if (g[u].size() > 2) {
for (int j = 1; j < (int)g[u].size(); ++j)
dsu::merge(g[u][j - 1], g[u][j]);
}
}
ans1[i] = (dsu::cnt[i] * i == n);
vector<int> vec;
for (auto u : buc[i]) {
if (g[u].size() == 2) {
if (dsu::sz[dsu::find(fa[u])] == 1) {
vis[u] = vis[fa[u]] = true;
dsu::merge(fa[u], g[u][g[u][0] == fa[u]]);
vec.emplace_back(fa[u]);
rec[fa[u]] = 1;
} else if (vis[fa[u]]) {
++rec[fa[u]];
}
}
}
if (dsu::cnt[i] * i + dsu::cnt[i + 1] * (i + 1) == n) {
ans2[i] = 1;
for (auto u : vec) ans2[i] = (ll)ans2[i] * (rec[u] + (i == 1)) % MOD;
}
for (auto u : buc[i]) {
if (g[u].size() == 2 && !vis[u]) {
for (int j = 1; j < (int)g[u].size(); ++j)
dsu::merge(g[u][j - 1], g[u][j]);
}
}
}
if (!k) {
cout << accumulate(ans1 + 1, ans1 + n, 0) << "\n";
} else {
int ans = MOD - 1;
for (int i = 1; i < n; ++i) inc(ans, ans2[i]);
for (int i = 2; i < n; ++i) dec(ans, ans1[i]);
cout << ans << "\n";
}
return 0;
}