[P9167] [省选联考 2023] 城市建造

发布时间 2023-04-13 17:52:35作者: JCY_std

洛谷题面

原题等价于计算有多少个点集 \(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\)\(\forall u \in S\),以 \(rt\) 为根时 \(u\) 的所有祖先方点都应属于 \(S\),证明同理。

证毕。

\(rt\) 为方点时以 \(rt\) 的任意相邻圆点为根,当 \(rt\) 为圆点时以 \(rt\) 为根,将圆方树定根。这样,对于符合题意\(S\)\(\forall 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 > S\)\(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\) 存在需要特殊处理的儿子,所以若 \(v \neq rt\)\(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;
}