连通性相关
连通性相关
强连通分量
有向强连通图:任意两个点可以互相到达。
强连通分量(SCC):极大的强连通子图。
Tarjan 算法
维护一个栈存储搜索到的还未确定强连通分量的点,定义:
- dfnu :节点 u 被搜索的次序。
- lowu :u 子树中能回溯到的最小的 dfn 。
不难得到:
- 一个点子树内的 dfn 大于该点的 dfn 。
- 从根开始的路径上的 dfn 递增,low 非降。
对于 u 的出点 v ,分类讨论:
-
v 未被访问过:继续 dfs ,并用 lowv 更新 lowu 。因为存在 u→v ,所以 v 可以直接回溯到已在栈中的点 u 一定可以回溯到。
-
v 被访问过
-
已在栈中:根据 low 的定义,用 dfnv 更新 lowu 。
-
不在栈中:说明 v 已搜索完毕,其所在的连通分量已被处理,不用管它。
-
对于一个强连通分量,不难发现只有一个 u 满足 dfnu=lowu ,其一定是这个强连通分量的根。
因此回溯过程中,若 dfnu=lowu ,则新增一个强连通分量。
void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v); low[u] = min(low[u], low[v]); } else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } }
对于一张有向图,其可能存在环。可以将每个强连通分量分别缩成一个点,这个图就会变成一张 DAG,可能会便于处理。
Kosaraju 算法
- 第一次 dfs:遍历所有点并在回溯时入栈。
- 第二次 dfs:在反图上依次从栈顶开始 dfs ,此时遍历到的点集就是一个强连通分量。
时间复杂度 O(n+m) 。
void dfs1(int u) { vis[u] = true; for (int v : G.e[u]) if (!vis[v]) dfs1(v); sta.emplace(u); } void dfs2(int u) { leader[u] = scc; for (int v : rG.e[u]) if (!leader[v]) dfs2(v); } inline void kosaraju() { for (int i = 1; i <= n; ++i) if (!vis[i]) dfs1(i); for (; !sta.empty(); sta.pop()) if (!leader[sta.top()]) ++scc, dfs(sta.top()); }
用 bitset
优化可以做到 O(n2ω) 的复杂度。
void dfs1(int u) { vis.set(u); bitset<N> now = ~vis & e1[u]; while (now.any()) dfs1(now._Find_first()), now &= ~vis; sta.push(u); } void dfs2(int u) { vis.set(u), leader[u] = scc; bitset<N> now = ~vis & e2[u]; while (now.any()) dfs2(now._Find_first()), now &= ~vis; } inline void kosaraju() { vis.reset(); for (int i = 1; i <= n; ++i) if (!vis.test(i)) dfs1(i); vis.reset(); for (; !sta.empty(); sta.pop()) if (!vis.test(sta.top())) ++scc, dfs(sta.top()); }
应用
BZOJ5218 省队十连测 友好城市
给出一张有向图,q 次询问仅保留编号属于 [li,ri] 的边时有多少无序对城市满足可以两两到达。
n≤150,m≤3×105,q≤5×104
注意到 n 很小,使用 Kosaraju 配合莫队即可,时间复杂度 O((√mlogm+q)n2ω+q√m) 。
#include <bits/stdc++.h> using namespace std; const int N = 1.5e2 + 7, M = 3e5 + 7, Q = 5e4 + 7; struct Edge { int u, v; } E[M]; struct Query { int l, r, *ans, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r < rhs.r : r > rhs.r) : bid < rhs.bid; } } qry[Q]; bitset<N> e1[N], e2[N]; bitset<N> vis; stack<int> sta; int cnt1[N][N], cnt2[N][N], ans[Q]; int n, m, q, block, scc; template <class T = int> inline T read() { char c = getchar(); bool sign = c == '-'; while (c < '0' || c > '9') c = getchar(), sign |= c == '-'; T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void Add(int x) { int u = E[x].u, v = E[x].v; if (!cnt1[u][v]) e1[u][v] = true; if (!cnt2[v][u]) e2[v][u] = true; ++cnt1[u][v], ++cnt2[v][u]; } inline void Del(int x) { int u = E[x].u, v = E[x].v; --cnt1[u][v], --cnt2[v][u]; if (!cnt1[u][v]) e1[u][v] = false; if (!cnt2[v][u]) e2[v][u] = false; } void dfs1(int u) { vis.set(u); bitset<N> now = ~vis & e1[u]; while (now.any()) dfs1(now._Find_first()), now &= ~vis; sta.push(u); } int dfs2(int u) { vis.set(u); bitset<N> now = ~vis & e2[u]; int siz = 1; while (now.any()) siz += dfs2(now._Find_first()), now &= ~vis; return siz; } inline int kosaraju() { vis.reset(), scc = 0; int res = 0; for (int i = 1; i <= n; ++i) if (!vis.test(i)) dfs1(i); vis.reset(); for (; !sta.empty(); sta.pop()) if (!vis.test(sta.top())) { int siz = dfs2(sta.top()); res += siz * (siz - 1) / 2; } return res; } signed main() { n = read(), m = read(), q = read(); block = sqrt(m); for (int i = 1; i <= m; ++i) E[i].u = read(), E[i].v = read(); for (int i = 1; i <= q; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].bid = qry[i].l / block, qry[i].ans = ans + i; sort(qry + 1, qry + 1 + q); for (int i = 1, l = 1, r = 0; i <= q; ++i) { while (l > qry[i].l) Add(--l); while (r < qry[i].r) Add(++r); while (l < qry[i].l) Del(l++); while (r > qry[i].r) Del(r--); *qry[i].ans = kosaraju(); } for (int i = 1; i <= q; ++i) printf("%d\n", ans[i]); return 0; }
CF1515G Phoenix and Odometers
给定一张带边权的有向图,q 次询问,每次给定 v,s,t ,询问时候存在一条经过 v 的回路满足长度与 −s 在模 t 意义下同余。
n,m,q≤2×105 。
首先不难发现每个 SCC 的答案是一致的,且不同 SCC 之间相互独立,故考虑对于每个 SCC 分开计算。
假设经过 u 有两个长度为 a 和 b 的环,那么就相当于找两个非负整数 x 和 y,使得 ax+by=w,其中 w 为题中的路径长,根据裴蜀定理得到上述方程成立当且仅当 gcd(a,b)∣w 。
考虑如何求出经过点 u 的所有环长度的 gcd 。通过分析发现,所有的非树边 u→v 对答案的贡献都是 disu+w−disv 。于是搜索时顺便记录贡献即可。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 2e5 + 7; struct Graph { vector<pair<int, int> > e[N]; inline void insert(const int u, const int v, const int w) { e[u].emplace_back(v, w); } } G; ll dis[N], g[N]; int dfn[N], low[N], sta[N], leader[N]; bool vis[N]; int n, m, q, dfstime, top, scc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline ll gcd(ll a, ll b) { if (!a || !b) return a | b; while (a ^= b ^= a ^= b %= a); return b; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (auto it : G.e[u]) { int v = it.first; if (!dfn[v]) { Tarjan(v); low[u] = min(low[u], low[v]); } else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (low[u] == dfn[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } } void dfs(int u, int cur) { vis[u] = true; for (auto it : G.e[u]) { int v = it.first, w = it.second; if (leader[v] != cur) continue; if (!vis[v]) dis[v] = dis[u] + w, dfs(v, cur); else g[cur] = gcd(g[cur], abs(dis[u] - dis[v] + w)); } } signed main() { n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(), w = read(); G.insert(u, v, w); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i); for (int i = 1; i <= n; ++i) if (!vis[i]) dfs(i, leader[i]); q = read(); while (q--) { int x = read(), s = read(), t = read(); if (g[leader[x]]) puts(s % gcd(g[leader[x]], t) ? "NO" : "YES"); else puts(s ? "NO" : "YES"); } return 0; }
CF1361E James and the Chase
给定一张有向强连通图。一个点是好的当且仅当它到其它点有且只有一条简单路径。如果好的点至少有 20% ,则输出所有好的点, 否则输出 −1 。
∑n≤105 ,∑m≤2×105
考虑如何判定 u 是好的。只要以 u 为根建出 dfs 树,若无横叉边或前向边则 u 即为好的。于是可以做到 O(n) 的判定。
接下来考虑确定一个好节点 u 后求出其余好节点。以 u 为根建立 dfs 树,考虑某个 v 的子树,由于整个图的强连通性,v 的子树中有连向其祖先的返祖边。不难发现这样的边有且仅有一条,否则 v 有两条路径可以到 fav 。
那么先把所有子树 v 内返祖到根的祖先的边的数量记录下来,如果数量 ≥2 则 v 不为好点,否则就顺便记录每个 v 子树的那条返祖边指向的点。
假设 v 的子树这条返祖边指向了 w,那么 v 是好点,当且仅当 w 是好点。
于是一个点 v 是好点,当且仅当 v 的子树内有且仅有一条连向 v 的祖先的返祖边,并且这条边所连向的点是好点。
第一个条件可以考虑所有返祖边 (a,b),它对哪些 v 的子树内返向 v 祖先的边的数量的有贡献。显然这样的 v 分布在 a→faa→...→sonb 上,这里的 sonb 为 b 的儿子节点中靠近 a 侧的那个,树上差分即可和第二个条件一起解决。
于是可以在已知一个好点的情况下线性求出所有好点。
考虑随机选取一定数量的点进行上述算法流程,若均不满足条件则输出 −1 ,注意随机取点不要取重。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Graph { vector<int> e[N]; inline void clear(int n) { for (int i = 1; i <= n; ++i) e[i].clear(); } inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; int cnt[N], pre[N], dep[N]; bool vis[N], in[N], ans[N]; mt19937 myrand(time(0)); int n, m; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } bool dfs(int u) { vis[u] = in[u] = true; for (int v : G.e[u]) { if (vis[v] && !in[v]) return false; else if (!in[v]) { dep[v] = dep[u] + 1, T.insert(u, v); if (!dfs(v)) return false; } else { ++cnt[u], --cnt[v]; if (dep[v] < dep[pre[u]]) pre[u] = v; } } in[u] = 0; return true; } inline bool check(int u) { memset(cnt + 1, 0, sizeof(int) * n); memset(pre + 1, 0, sizeof(int) * n); memset(vis + 1, false, sizeof(bool) * n); memset(in + 1, false, sizeof(bool) * n); T.clear(n), dep[u] = 1, dep[0] = n + 1; return dfs(u); } inline void calc(int u) { for (int v : T.e[u]) { calc(v), cnt[u] += cnt[v]; if (dep[pre[v]] < dep[pre[u]]) pre[u] = pre[v]; } } void solve(int u) { if(cnt[u] == 1 && dep[pre[u]] < dep[u] && ans[pre[u]]) ans[u] = true; for (int v : T.e[u]) solve(v); } signed main() { int T = read(); while (T--) { n = read(), m = read(); G.clear(n); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v); } vector<int> id(n); iota(id.begin(), id.end(), 1); shuffle(id.begin(), id.end(), myrand); int root = -1; for (int i = 0; i < min(n, 100); ++i) if (check(id[i])) { root = id[i]; break; } if (root == -1) { puts("-1"); continue; } calc(root); memset(ans + 1, false, sizeof(bool) * n); ans[root] = true, solve(root); if (count(ans + 1, ans + 1 + n, true) * 5 < n) puts("-1"); else { for (int i = 1; i <= n; ++i) if (ans[i]) printf("%d ", i); puts(""); } } return 0; }
2-SAT
2-SAT 问题:给定一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。
实现
构造状态
点的状态:将点 u 拆分成 u0,u1 两个点,分别表示 u 点为假、真。
边的状态:若连的边为 u→v ,就表示选择 u 就必须选 v 。
判断有无解
由所构造的状态可知,对于图中的每一个强连通分量,如果选择了其中任意一个点,那就意味着这个强连通分量中的所有点都要选。显然 x0,x1 不可以同时选,由此可判断有无解。
方案输出
由连边的方式可以得知,我们对于每个点的两种状态,选择拓扑序大的,舍弃掉另一个。
注意到用 Tarjan 求得的强连通分量编号就是反拓扑序,于是选择强连通分量编号较小的点即可。
如果要求字典序最小,就深搜枚举点 1→2n ,贪心选取。
#include <bits/stdc++.h> using namespace std; const int N = 2e6 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; int dfn[N], low[N], sta[N], leader[N]; int n, m, dfstime, top, scc; void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) Tarjan(v), low[u] = min(low[u], low[v]); else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (low[u] == dfn[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } } signed main() { scanf("%d%d", &n, &m); for (int i = 1; i <= m; ++i) { int u, x, v, y; scanf("%d%d%d%d", &u, &x, &v, &y); G.insert(u + (x ^ 1) * n, v + y * n); G.insert(v + (y ^ 1) * n, u + x * n); } for (int i = 1; i <= n * 2; ++i) if (!dfn[i]) Tarjan(i); for (int i = 1; i <= n; ++i) if (leader[i] == leader[i + n]) return puts("IMPOSSIBLE"), 0; puts("POSSIBLE"); for (int i = 1; i <= n; ++i) printf("%d ", leader[i + n] < leader[i]); return 0; }
应用
P3825 [NOI2017] 游戏
给定一串序列,有 d 个 x 位置有三种选择,其他位置有两种选择,求解一种合法方案。
n≤5×104,d≤8
暴力枚举每个 x 地图不填 A 或不填 B 。因为不填 A 就可以填 B,C ,不填 B 就可以填 A,C ,这样就包含了 A,B,C 三种赛车。
时间复杂度降为 O((n+m)×2d) 。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Graph { vector<int> e[N]; inline void clear(int n) { for (int i = 1; i <= n; ++i) e[i].clear(); } inline void insert(const int u, const int v) { e[u].emplace_back(v); } } G; struct Node { int x, y; char cx, cy; } nd[N]; int dfn[N], low[N], leader[N], sta[N]; char str[N]; int n, d, m, dfstime, top, scc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline char readc() { char c = getchar(); while (c < 'A' || c > 'Z') c = getchar(); return c; } inline int trans(int x) { return x <= n ? x + n : x - n; } inline int getid(int x, char op) { if (str[x] == 'a') return op == 'b' ? x : x + n; else return op == 'a' ? x : x + n; } inline void clear(int n) { G.clear(n); memset(dfn + 1, 0, sizeof(int) * n); memset(low + 1, 0, sizeof(int) * n); memset(leader + 1, 0, sizeof(int) * n); dfstime = scc = 0; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v); low[u] = min(low[u], low[v]); } else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } } inline bool solve() { clear(n * 2); for (int i = 1; i <= m; ++i) { if (str[nd[i].x] == nd[i].cx) continue; int x = getid(nd[i].x, nd[i].cx), y = getid(nd[i].y, nd[i].cy); if (str[nd[i].y] == nd[i].cy) G.insert(x, trans(x)); else G.insert(x, y), G.insert(trans(y), trans(x)); } for (int i = 1; i <= n * 2; ++i) if (!dfn[i]) Tarjan(i); for (int i = 1; i <= n; ++i) if (leader[i] == leader[i + n]) return false; return true; } bool dfs(int pos) { if (pos > n) return solve(); else if (str[pos] != 'x') return dfs(pos + 1); for (int i = 0; i < 2; ++i) { str[pos] = 'a' + i; if (dfs(pos + 1)) return true; } return str[pos] = 'x', false; } signed main() { n = read(), d = read(); scanf("%s", str + 1); m = read(); for (int i = 1; i <= m; ++i) { nd[i].x = read(), nd[i].cx = tolower(readc()); nd[i].y = read(), nd[i].cy = tolower(readc()); } if (!dfs(1)) return puts("-1"), 0; for (int i = 1; i <= n; ++i) if (str[i] == 'a') putchar(leader[i] < leader[i + n] ? 'B' : 'C'); else if (str[i] == 'b') putchar(leader[i] < leader[i + n] ? 'A' : 'C'); else putchar(leader[i] < leader[i + n] ? 'A' : 'B'); return 0; }
[ARC161E] Not Dyed by Majority (Cubic Graph)
给出每个点的度数恰为 3 的无向图。一次操作为将每个点的颜色变为所有邻居颜色的众数。构造黑白颜色序列使得无论如何染色,操作后都不可能变为该颜色序列。
∑n≤5×104 ,保证 n 为偶数
考虑如何判定一个颜色序列是否可作为操作后的颜色序列。
设操作前的颜色序列为 ai(不妨使 ai=0,1 代表黑和白),操作后的颜色序列为 bi 。对于点 i,ei,j 为与 i 相邻的三个点。
对每个 i 有两种情况:
- bi=0:则对 j≠k,若 aei,j=1 则 aei,k=0 。
- bi=1:则对 j≠k,若 aei,j=0 则 aei,k=1 。
这是一个关于 ai,有 6n 个条件的 2-SAT 问题,可以 O(n) 解决,目标转化为找到一个方案使得 2-SAT 问题无解。
考虑 1 的邻域 x,y,z,以及他们的邻域 {1,x0,x1},{1,y0,y1},{1,z0,z1} ,若 dx0=dx1,dy0=dy1,dz0=dz1 ,那么无论 d1 是什么,答案都不会更改。因此至少有 116 的序列会互相重复,那么期望 O(1) 次随机后能得到一组解。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Graph { vector<int> e[N]; inline void clear(int n) { for (int i = 1; i <= n; ++i) e[i].clear(); } inline void insert(int u, int v) { e[u].emplace_back(v); } } G, nG; int col[N], dfn[N], low[N], sta[N], leader[N]; mt19937 myrand(time(0)); int n, dfstime, scc, top; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : nG.e[u]) { if (!dfn[v]) { Tarjan(v); low[u] = min(low[u], low[v]); } else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (low[u] == dfn[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } } signed main() { int T = read(); while (T--) { G.clear(n = read()); for (int i = 1; i <= n / 2 * 3; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (;;) { nG.clear(n * 2); for (int u = 1; u <= n; ++u) { col[u] = rand() & 1; for (int v : G.e[u]) for (int w : G.e[u]) if (v != w) { if (col[u]) nG.insert(v, w + n); else nG.insert(v + n, w); } } memset(dfn + 1, 0, sizeof(int) * (n * 2)); memset(low + 1, 0, sizeof(int) * (n * 2)); memset(leader + 1, 0, sizeof(int) * (n * 2)); dfstime = scc = 0; for (int i = 1; i <= n * 2; ++i) if (!dfn[i]) Tarjan(i); bool flag = false; for (int i = 1; i <= n; ++i) if (leader[i] == leader[i + n]) { flag = true; break; } if (flag) break; } for (int i = 1; i <= n; ++i) putchar(col[i] ? 'B' : 'W'); puts(""); } return 0; }
滈葕
给定一张边权为 0 或 1 的有向图,给每个点赋予 A/B/C/D 的一个字母,使得每条有向边边权为 1 是 (au,av)∈{(A,D),(A,B),(B,D),(B,A),(C,D),(C,A),(C,B)} 的充要条件。
n≤105 ,m≤5×105
考虑令 A=01,B=10,C=11,D=11 ,于是每一条 1 的边都对应了存在某一位 u>v 。考虑如何表示为 2-SAT 的限制:
- w=0 :对于每一位 u 都不大于 v 。
- 若 u 的某一位是 1 ,则 v 的相应位必须是 1 。
- 若 v 的某一位是 0 ,则 u 的相应位必须是 0 。
- w=1 :存在每一位 u 大于 v 。
- 若 u 的某一位是 0 ,则 u 的另一位必须是 1 ,且 v 的另一位必须是 0 。
- 若 v 的某一位是 1 ,则 v 的另一位必须是 0 ,且 u 的另一位必须是 1 。
时间复杂度 O(n) 。
#include <bits/stdc++.h> using namespace std; const int N = 4e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; int dfn[N], low[N], sta[N], leader[N]; int n, m, dfstime, top, scc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) Tarjan(v), low[u] = min(low[u], low[v]); else if (!leader[v]) low[u] = min(low[u], dfn[v]); } if (low[u] == dfn[u]) { ++scc; while (sta[top] != u) leader[sta[top--]] = scc; leader[sta[top--]] = scc; } } signed main() { n = read(), m = read(); // x, x + n * 2 : bit0 = 0 / 1 // x + n, x + n * 3 : bit1 = 0 / 1 for (int i = 1; i <= m; ++i) { int u = read(), v = read(), w = read(); if (w) { G.insert(u, u + n * 3), G.insert(u, v + n); G.insert(u + n, u + n * 2), G.insert(u + n, v); G.insert(v + n * 2, v + n), G.insert(v + n * 2, u + n * 3); G.insert(v + n * 3, v), G.insert(v + n * 3, u + n * 2); } else { G.insert(u + n * 2, v + n * 2), G.insert(u + n * 3, v + n * 3); G.insert(v, u), G.insert(v + n, u + n); } } for (int i = 1; i <= n * 4; ++i) if (!dfn[i]) Tarjan(i); for (int i = 1; i <= n * 2; ++i) if (leader[i] == leader[i + n * 2]) return puts("NO"), 0; puts("YES"); string str = "DABC"; for (int i = 1; i <= n; ++i) putchar(str[(leader[i + n * 2] < leader[i]) + (leader[i + n * 3] < leader[i + n]) * 2]); return 0; }
割点
定义:对于一个无向图,若把一个点删除后这个图的极大连通分量增加了,则这个点就是图的一个割点。
特判根节点。对于非根节点,若存在一个儿子点 v 使得 lowv≥dfnu (即不能回到祖先),则该点为割点。
void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; int sonsum = 0; for (int v : G.e[u]) { if (!dfn[v]) { ++sonsum, Tarjan(v, u); cut[u] |= (f && low[v] >= dfn[u]); low[u] = min(low[u], low[v]); } else low[u] = min(low[u], dfn[v]); } cut[u] |= (!f && sonsum >= 2); }
桥(割边)
定义:对于一个无向图,若把一条边删除后这个图的极大连通分量增加了,则这条边就是图的一个桥。
代码和割点差不多,只要改一处: lowv>dfnu ,而且不用特判根节点。
void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } }
边双连通
- 边双连通:无向图中对于两点 u,v ,若满足删去任意边两点均连通,则称 u,v 边双连通。
- 边双连通分量:不存在割边的极大连通块。
求解
遍历时不走割边即可求得边双。
也可以用栈维护 dfs 到的所有点,每次找到割边 (fa,son) 就不断弹栈直到弹出 son ,则弹出的所有点是一个边双。
#include <bits/stdc++.h> using namespace std; const int N = 5e5 + 7, M = 4e6 + 7; struct Graph { struct Edge { int nxt, v; bool cut; } e[M]; int head[N]; int tot = 1; inline void insert(int u, int v) { e[++tot] = (Edge){head[u], v, false}, head[u] = tot; } } G; vector<int> edcc[N]; int dfn[N], low[N]; bool vis[N]; int n, m, dfstime, tot; void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } } void dfs(int u) { edcc[tot].emplace_back(u), vis[u] = true; for (int i = G.head[u]; i; i = G.e[i].nxt) if (!G.e[i].cut && !vis[G.e[i].v]) dfs(G.e[i].v); } signed main() { scanf("%d%d", &n, &m); for (int i = 1; i <= m; ++i) { int u, v; scanf("%d%d", &u, &v); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i, 0); for (int i = 1; i <= n; ++i) if (!vis[i]) ++tot, dfs(i); printf("%d\n", tot); for (int i = 1; i <= tot; ++i) { printf("%d ", (int)edcc[i].size()); for (int it : edcc[i]) printf("%d ", it); puts(""); } return 0; }
相关结论
- 边双对点有传递性。
- 每个点恰属于一个边双。
- 对于边双内任意一条边 e ,存在经过 e 的回路。
- 对于边双内任意两点 u,v ,存在经过 u,v 的简单环。
- 两点之间任意一条迹(不经过重复边的路径)上的所有割边,就是两点之间的所有必经边。
- u,v 边双连通当且仅当 u,v 间无必经边。
- 边双可以赋方向后可以变成 SCC。
- 边双缩点完是一棵树/森林。
应用
P4652 [CEOI2017] One-Way Streets
给定一张 n 个点 m 条边的无向图,现在想要把这张图定向。
有 q 个限制条件,每个条件形如 (xi,yi),表示在新的有向图当中,xi 要能够沿着一些边走到 yi。
现在请你求出,每条边的方向是否能够唯一确定。同时请给出这些能够唯一确定的边的方向。数据保证有解。
n,m,q≤105
由于边双可以赋方向后可以变成 SCC,那么反向后仍然是 SCC,因此一个边双内的边的方向都是无法确定的。
将边双缩点,构建出一棵树,那么一个限制就是树上定向一条路径,不难用树上差分解决。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Graph { struct Edge { int nxt, v; bool cut; } e[N << 1]; int head[N]; int tot = 1; inline void insert(int u, int v) { e[++tot] = (Edge) {head[u], v, false}, head[u] = tot; } } G, T; int dfn[N], low[N], leader[N], val[N], fa[N]; char ans[N]; int n, m, q, dfstime, edcc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } } void dfs1(int u) { leader[u] = edcc; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!G.e[i].cut && !leader[v]) dfs1(v); } } void dfs2(int u, int f) { fa[u] = f; for (int i = T.head[u]; i; i = T.e[i].nxt) { int v = T.e[i].v; if (v != f) dfs2(v, u), val[u] += val[v]; } } signed main() { n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i, 0); for (int i = 1; i <= n; ++i) if (!leader[i]) ++edcc, dfs1(i); for (int u = 1; u <= n; ++u) for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (leader[u] != leader[v]) T.insert(leader[u], leader[v]); } q = read(); while (q--) ++val[leader[read()]], --val[leader[read()]]; for (int i = 1; i <= edcc; ++i) if (!fa[i]) dfs2(i, 0); for (int u = 1; u <= n; ++u) for (int i = G.head[u]; i; i = G.e[i].nxt) { if (i & 1) continue; int v = G.e[i].v; if (leader[u] == leader[v]) ans[i / 2] = 'B'; else if (fa[leader[v]] == leader[u]) ans[i / 2] = (val[leader[v]] ? (val[leader[v]] > 0 ? 'L' : 'R') : 'B'); else ans[i / 2] = (val[leader[u]] ? (val[leader[u]] > 0 ? 'R' : 'L') : 'B'); } ans[m + 1] = '\0', puts(ans + 1); return 0; }
CF475E Strongly Connected City 2
给一个无向连通图,需要给所有边定向,最大化满足 a 能到达 b 的点对 (a,b) 数量。
n≤2000
由于边双可以赋方向后可以变成 SCC,于是考虑把先把边双缩点成树。
可以发现最优方案一定是确定根之后,一部分子树都是叶向边,剩下的子树都是父向边,并且这两部分的大小要尽可能接近。
直接枚举根做 bitset
优化背包即可做到 O(n+m+n2ω) 。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const ll inf = 0x3f3f3f3f3f3f3f3f; const int N = 2e3 + 7, M = 4e6 + 7; struct Graph { struct Edge { int nxt, v; bool cut; } e[M]; int head[N]; int tot = 1; inline void insert(int u, int v) { e[++tot] = (Edge) {head[u], v, false}, head[u] = tot; } } G, T; int dfn[N], low[N], leader[N], a[N], siz[N]; int n, m, dfstime, edcc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } } void dfs1(int u) { ++a[leader[u] = edcc]; for (int i = G.head[u]; i; i = G.e[i].nxt) if (!leader[G.e[i].v] && !G.e[i].cut) dfs1(G.e[i].v); } void dfs2(int u, int fa) { siz[u] = a[u]; for (int i = T.head[u]; i; i = T.e[i].nxt) { int v = T.e[i].v; if (v != fa) dfs2(v, u), siz[u] += siz[v]; } } signed main() { n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i, 0); for (int i = 1; i <= n; ++i) if (!leader[i]) ++edcc, dfs1(i); for (int u = 1; u <= n; ++u) for (int i = G.head[u]; i; i = G.e[i].nxt) if (G.e[i].cut) T.insert(leader[u], leader[G.e[i].v]); ll ans = 0; for (int u = 1; u <= edcc; ++u) { dfs2(u, 0); ll res = 0; for (int i = 1; i <= edcc; ++i) res += 1ll * siz[i] * a[i]; bitset<N> f; f.set(0); for (int i = T.head[u]; i; i = T.e[i].nxt) f |= f << siz[T.e[i].v]; for (int i = 0; i <= n - a[u]; ++i) if (f.test(i)) ans = max(ans, res + 1ll * i * (n - a[u] - i)); } printf("%lld", ans); return 0; }
CF51F Caterpillar
一个无向连通无环图是毛毛虫当且仅当图上存在一条路径 P 使得任意点与 P 的距离至多为 1 ,要求 P 不能有重边。
给出一张无向图,一次操作可以将两个点合并(出边也合并),求将其变成毛毛虫的最少操作次数。
n≤2000 ,m≤105
由于毛毛虫无环,因此先对整张图按边双缩点转化为森林。
对于一棵树,最优的毛毛虫显然就是去掉叶子之后的直径,因为叶子可以变成一条链上挂的点,剩下的最长链显然就是直径。
时间复杂度 O(n+m) 。
#include <bits/stdc++.h> using namespace std; const int N = 2e3 + 7, M = 2e5 + 7; struct Graph { struct Edge { int nxt, v; bool cut; } e[M]; int head[N]; int tot = 1; inline void insert(int u, int v) { e[++tot] = (Edge) {head[u], v, false}, head[u] = tot; } } G, T; int dfn[N], low[N], leader[N], len[N], son[N], deg[N]; bool vis[N], in[N]; int n, m, dfstime, edcc; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } } void dfs1(int u) { leader[u] = edcc; for (int i = G.head[u]; i; i = G.e[i].nxt) if (!leader[G.e[i].v] && !G.e[i].cut) dfs1(G.e[i].v); } int dfs2(int u, int fa) { vis[u] = true, len[u] = 1, son[u] = 0; int down = u; for (int i = T.head[u]; i; i = T.e[i].nxt) { int v = T.e[i].v; if (v == fa) continue; int x = dfs2(v, u); if (len[v] + 1 > len[u]) down = x, son[u] = v, len[u] = len[v] + 1; } return down; } signed main() { n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i, 0); for (int i = 1; i <= n; ++i) if (!leader[i]) ++edcc, dfs1(i); for (int u = 1; u <= n; ++u) for (int i = G.head[u]; i; i = G.e[i].nxt) if (G.e[i].cut) T.insert(leader[u], leader[G.e[i].v]), ++deg[leader[u]]; int ans = count(deg + 1, deg + edcc + 1, 1), cnt = 0; for (int i = 1; i <= edcc; ++i) if (!vis[i]) { ++cnt; if (!deg[i]) { ++ans; continue; } int rt = dfs2(i, 0); dfs2(rt, 0), ans += len[rt] - 2; } printf("%d", n - ans + cnt - 1); return 0; }
CF855G Harry Vs Voldemort
给出一棵树,q 次增加一条无向边的操作,每次求有多少个三元组 (u,v,w) ,满足 P(u→w)∩P(v→w)=∅ ,其中 P 表示一条路径的边集。
n,q≤105
考虑缩完边双的树,枚举 w 为根,则满足条件的 (u,v,w) 必然满足 u 和 v 不在 w 的同一子树内。定一个根之后,一个 w 的贡献就是 n2−(n−sw)2−∑v∈son(w)s2v ,其中 su 表示 u 的带权子树大小。
接下来考虑处理加边的情况,考虑用并查集维护边双,一个点指向每个边双深度最小的节点,每次暴力向上跳父亲合并边双,同时维护 s 和子树的 s2 即可。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 1e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; ll s2[N]; int fa[N], dep[N], s1[N]; ll ans = 0; int n, q; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void calc(int x, int s, int op) { ans += 1ll * op * s * (s - 1) * (s - 2); // u, v \in edcc ans += 2ll * op * s * (s - 1) * (n - s); // u (or v) \not \in edcc ans += 1ll * op * s * (1ll * (n - s) * (n - s) - s2[x] - 1ll * (n - s1[x]) * (n - s1[x])); // u, v \not \in edcc } struct DSU { int fa[N], siz[N]; inline void prework(int n) { iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1); } inline int find(int x) { while (x != fa[x]) fa[x] = fa[fa[x]], x = fa[x]; return x; } inline void merge(int x, int y) { x = find(x), y = find(y); calc(x, siz[x], -1), calc(y, siz[y], -1); siz[x] += siz[y], s2[x] += s2[y] - 1ll * s1[y] * s1[y], fa[y] = x; calc(x, siz[x], 1); } } dsu; void dfs(int u, int f) { fa[u] = f, dep[u] = dep[f] + 1, s1[u] = 1; for (int v : G.e[u]) if (v != f) dfs(v, u), s1[u] += s1[v], s2[u] += 1ll * s1[v] * s1[v]; calc(u, 1, 1); } signed main() { n = read(); for (int i = 1; i < n; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } dfs(1, 0), dsu.prework(n); printf("%lld\n", ans); q = read(); while (q--) { int x = read(), y = read(); while (dsu.find(x) != dsu.find(y)) { x = dsu.find(x), y = dsu.find(y); if (dep[x] < dep[y]) swap(x, y); dsu.merge(fa[x], x); } printf("%lld\n", ans); } return 0; }
P8867 [NOIP2022] 建造军营
给出一张无向连通图,求有多少选出至少一个点组成点集 V′ 和任意数量边组成边集 E′ 的方案,满足对于任意 e∈E∖E′ 满足删去 e 后 V′ 仍连通。
n≤5×105 ,m≤106
先将边双缩点,变成一棵树。则选出的点集在树上组成的极小连通块的树边必须选,剩下的边无所谓。
于是可以树形 DP,设 fu,0/1 表示 u 子树是否选点的方案数,若选点则必须通过选树边与 u 连通。考虑转移,有:
接下来考虑统计答案,每个点集连通块在 LCA (记为 u )处统计,钦定 u 连父亲的边不能选(否则会在父亲再统计一次),则令 ans←fu,1×2m−se(u)−1 ,其中 se(u) 表示 u 子树内的边数,对根特殊处理 ans←fr,1 即可。
#include <bits/stdc++.h> using namespace std; const int Mod = 1e9 + 7; const int N = 5e5 + 7, M = 1e6 + 7; struct Graph { struct Edge { int nxt, v; bool cut; } e[M << 1]; int head[N]; int tot = 1; inline void insert(int u, int v) { e[++tot] = (Edge){head[u], v, false}, head[u] = tot; } } G, T; int f[N][2], pw[M]; int dfn[N], low[N], leader[N], vsiz[N], esiz[N]; int n, m, dfstime, edcc, ans; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline int add(int x, int y) { x += y; if (x >= Mod) x -= Mod; return x; } inline int dec(int x, int y) { x -= y; if (x < 0) x += Mod; return x; } void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) G.e[i].cut = G.e[i ^ 1].cut = true; } else if (v != f) low[u] = min(low[u], dfn[v]); } } void dfs1(int u) { ++vsiz[leader[u] = edcc]; for (int i = G.head[u]; i; i = G.e[i].nxt) if (!G.e[i].cut && !leader[G.e[i].v]) dfs1(G.e[i].v); } void dfs2(int u, int fa) { f[u][0] = pw[esiz[u]], f[u][1] = 1ll * dec(pw[vsiz[u]], 1) * pw[esiz[u]] % Mod; for (int i = T.head[u]; i; i = T.e[i].nxt) { int v = T.e[i].v; if (v == fa) continue; dfs2(v, u), esiz[u] += esiz[v] + 1; f[u][1] = add(1ll * f[u][0] * f[v][1] % Mod, 1ll * f[u][1] * add(2ll * f[v][0] % Mod, f[v][1]) % Mod); f[u][0] = 2ll * f[u][0] * f[v][0] % Mod; } if (u == 1) ans = add(ans, f[u][1]); else ans = add(ans, 1ll * f[u][1] * pw[m - esiz[u] - 1] % Mod); } signed main() { scanf("%d%d", &n, &m); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } Tarjan(1, 0); for (int i = 1; i <= n; ++i) if (!leader[i]) ++edcc, dfs1(i); for (int u = 1; u <= n; ++u) for (int i = G.head[u]; i; i = G.e[i].nxt) { if (G.e[i].cut) T.insert(leader[u], leader[G.e[i].v]); else ++esiz[leader[u]]; } for (int i = 1; i <= edcc; ++i) esiz[i] >>= 1; pw[0] = 1; for (int i = 1; i <= m; ++i) pw[i] = 2ll * pw[i - 1] % Mod; dfs2(1, 0); printf("%d", ans); return 0; }
两条
给出一张无向图,q 次操作:
1 k
:删除第 k 条边。2 u v
:查询 u,v 之间是否存在两条边不相交路径。n≤8×105 ,m,q≤106
考虑时光倒流,则每次都是加入一条边。若能求出整个图的一棵生成树,则每次可以用并查集缩边双。
考虑求出一棵生成树,用并查集维护,先将始终存在的边加入,然后倒序枚举,若枚举到的边会合并两个连通块,则说明其为一条割边,删除或存在不影响答案,因此也将其加入。
总时间复杂度 O(n) 。
#include <bits/stdc++.h> using namespace std; const int N = 8e5 + 7, M = 1e6 + 7; struct Edge { int u, v; } e[M]; struct Node { int op, x, y; } nd[M]; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; struct DSU { int fa[N]; inline void prework(int n) { iota(fa + 1, fa + 1 + n, 1); } inline int find(int x) { while (x != fa[x]) fa[x] = fa[fa[x]], x = fa[x]; return x; } inline void merge(int x, int y) { x = find(x), y = find(y); if (x != y) fa[y] = x; } } dsu; int fa[N], dep[N]; bool exist[M], ans[M]; int n, m, q, testid; void dfs(int u, int f) { fa[u] = f, dep[u] = dep[f] + 1; for (int v : G.e[u]) if (v != f) dfs(v, u); } inline void update(int x, int y) { x = dsu.find(x), y = dsu.find(y); while (x != y) { if (dep[x] < dep[y]) swap(x, y); dsu.merge(fa[x], x), x = dsu.find(x); } } signed main() { scanf("%d%d%d%d", &n, &m, &q, &testid); for (int i = 1; i <= m; ++i) scanf("%d%d", &e[i].u, &e[i].v), exist[i] = true; for (int i = 1; i <= q; ++i) { scanf("%d", &nd[i].op); if (nd[i].op == 1) scanf("%d", &nd[i].x), exist[nd[i].x] = false; else scanf("%d%d", &nd[i].x, &nd[i].y); } dsu.prework(n); for (int i = 1; i <= m; ++i) if (exist[i]) { int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v); if (fx != fy) { G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u); dsu.merge(fx, fy), exist[i] = false; } } for (int i = q; i; --i) if (nd[i].op == 1) { int fx = dsu.find(e[nd[i].x].u), fy = dsu.find(e[nd[i].x].v); if (fx != fy) { G.insert(e[nd[i].x].u, e[nd[i].x].v), G.insert(e[nd[i].x].v, e[nd[i].x].u); dsu.merge(fx, fy), nd[i].op = -1; } } for (int i = 1; i <= n; ++i) if (!dep[i]) dfs(i, 0); dsu.prework(n); for (int i = 1; i <= m; ++i) if (exist[i]) update(e[i].u, e[i].v); for (int i = q; i; --i) { if (nd[i].op == 1) update(e[nd[i].x].u, e[nd[i].x].v); else if (nd[i].op == 2) ans[i] = (dsu.find(nd[i].x) == dsu.find(nd[i].y)); } for (int i = 1; i <= q; ++i) if (nd[i].op == 2) puts(ans[i] ? "YES" : "NO"); return 0; }
点双连通
- 点双连通:无向图中对于两点 u,v ,若满足删去任意除这两点以外的点两点均连通,则称 u,v 点双连通。
- 点双连通分量:不存在割点的极大连通块。
求解
每次判定 lowv≥dfnu 时,v 子树栈内的点与 u 共同构成一个点双,需要特判一下孤立点的情况。
#include <bits/stdc++.h> using namespace std; const int N = 5e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; vector<int> vdcc[N]; int dfn[N], low[N], sta[N]; int n, m, dfstime, top, tot; void Tarjan(int u, int f) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; int sonsum = 0; for (int v : G.e[u]) { if (!dfn[v]) { ++sonsum, Tarjan(v, u), low[u] = min(low[u], low[v]); if (low[v] >= dfn[u]) { vdcc[++tot] = {u}; while (sta[top] != v) vdcc[tot].emplace_back(sta[top--]); vdcc[tot].emplace_back(sta[top--]); } } else if (v != f) low[u] = min(low[u], dfn[v]); } if (!f && !sonsum) vdcc[++tot] = {u}; } signed main() { scanf("%d%d", &n, &m); for (int i = 1; i <= m; ++i) { int u, v; scanf("%d%d", &u, &v); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i, 0); printf("%d\n", tot); for (int i = 1; i <= tot; ++i) { printf("%d ", (int)vdcc[i].size()); for (int it : vdcc[i]) printf("%d ", it); puts(""); } return 0; }
相关结论
- 点双对点不具有传递性。
- 每条边恰属于一个点双。
- 一个点是割点当且仅当它属于多个点双。
- 由一条边直接相连的两个点点双连通。
- 对于点双内的任意点 u ,存在经过 u 的简单环。
- 当 n≥3 时,在边中间插入点不影响点双连通性,因此钦定经过一个点和经过一条边是几乎等价的。
- 对 n≥3 的点双中任意点 u 与任意边 e ,存在经过 u,e 的简单环。
- 对 n≥3 的点双中任意不同两点 u,v 与任意边 e ,存在 u⇝e⇝v 的简单路径。
- 对 n≥3 的点双中任意不同三点 u,v,w ,存在 u⇝v⇝w 的简单路径。
- 两点之间任意一条路径上的所有割点,就是两点之间的所有必经点。
- 若两点双有交,那么交点一定是割点。
应用
P8456 「SWTR-8」地地铁铁
给定边权为 0 或 1 的无向连通图,求有多少组点对之间存在同时经过 0 和 1 的简单路径。
n≤4×105 ,m≤106
对于落在不同点双的点对,如果经过的点双包含 0 边和 1 边则合法,否则显然不合法。
对于落在相同点双的点对,如果点双内部边权相同,显然不合法,否则若存在唯二既有 0 出边又有 1 出边的点对,则该点对不合法,其他合法。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 4e5 + 7, M = 1e6 + 7; struct DSU { int fa[N << 1], siz[N << 1]; inline void prework(int n, int ext) { iota(fa + 1, fa + 1 + n + ext, 1); fill(siz + 1, siz + 1 + n, 1), fill(siz + 1 + n, siz + 1 + n + ext, 0); } inline int find(int x) { while (x != fa[x]) fa[x] = fa[fa[x]], x = fa[x]; return x; } inline void merge(int x, int y) { x = find(x), y = find(y); if (x == y) return; fa[y] = x, siz[x] += siz[y]; } } dsu1, dsu2; struct Graph { struct Edge { int nxt, v, w; } e[M << 1]; int head[N]; int tot = 1; inline void insert(int u, int v, int w) { e[++tot] = (Edge) {head[u], v, w}, head[u] = tot; } } G; vector<int> bscc[N]; int dfn[N], low[N], sta[N], esta[M], tag[N], tp[N]; bool in[M]; ll ans; int testid, n, m, dfstime, top, etop, ext; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline char readc() { char c = getchar(); while (c != 'd' && c != 'D') c = getchar(); return c; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int i = G.head[u]; i; i = G.e[i].nxt) { int v = G.e[i].v; if (!dfn[v]) { in[esta[++etop] = i >> 1] = true; Tarjan(v); low[u] = min(low[u], low[v]); if (low[v] >= dfn[u]) { bscc[++ext].emplace_back(u); while (sta[top] != v) bscc[ext].emplace_back(sta[top--]); bscc[ext].emplace_back(sta[top--]); vector<int> E; while (esta[etop] != (i >> 1)) E.emplace_back(esta[etop--]); E.emplace_back(esta[etop--]); for (int x : E) { in[x] = false; tp[ext] |= 1 << G.e[x << 1].w; tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w; tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w; } int sum = 0; for (int x : bscc[ext]) sum += (tag[x] == 3), tag[x] = 0; if (sum == 2) --ans; } } else { low[u] = min(low[u], dfn[v]); if (dfn[v] < dfn[u] && !in[i >> 1]) esta[++etop] = i >> 1; } } } signed main() { testid = read(), n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(), w = (readc() == 'd'); G.insert(u, v, w), G.insert(v, u, w); } ans = 1ll * n * (n - 1) / 2; Tarjan(1); dsu1.prework(n, ext), dsu2.prework(n, ext); for (int u = 1; u <= ext; ++u) for (int v : bscc[u]) if (tp[u] == 1) dsu1.merge(u + n, v); else if (tp[u] == 2) dsu2.merge(u + n, v); for (int i = 1; i <= n + ext; ++i) { if (dsu1.find(i) == i) ans -= 1ll * dsu1.siz[i] * (dsu1.siz[i] - 1) / 2; if (dsu2.find(i) == i) ans -= 1ll * dsu2.siz[i] * (dsu2.siz[i] - 1) / 2; } printf("%lld", ans); return 0; }
圆方树
此处定义点双为不存在割点的极大联通分量。这与点双的原始定义只在两个点一条边时有区别。
在圆方树中,原来的每个点对应一个圆点,每一个点双对应一个方点。所以共有 n+c 个点,其中 n 是原图点数,c 是原图点双连通分量的个数。
而对于每一个点双,它对应的方点向这个点双中的每个点连边。每个点双形成一个菊花图,多个菊花图通过原图中的割点连接在一起(因为点双的分隔点是割点)。
显然,圆方树中每条边连接一个圆点和一个方点。
圆方树的点数小于 2n ,一定要开两倍空间。
只有当原图连通的情况下圆方树才是树。
构建
考虑 Tarjan 算法。不难发现对于一条边 u→v ,u,v 在同一个点双中且 u 是该点双中深度最浅的节点当且仅当 lowv=dfnu 。类似 Tarjan 求割点的方式构建圆方树即可。
void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } }
应用
P4630 [APIO2018] 铁人两项
给定一张简单无向图,求有多少对三元组 (s,c,f) 满足 s,c,f 互异且存在一条简单路径从 s 出发经过 c 到达 f 。
n≤105
首先不难发现,若经过一个点双(至少两个点),则可以到达该点双中的任意点再走出来。
考虑构建圆方树,对于一对 s,f ,合法的 c 就是 s 到 f 路径上所有圆点和方点连的圆点,再去掉 s,f 。
令圆点权值为 −1 (因为会被路径上相邻两个方点统计两次),方点权值为点双大小,则一对 s,f 的贡献就是树上路径权值和。不难树上换根 DP 处理,时间复杂度 O(n+m) 。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 2e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; int dfn[N], low[N], sta[N], siz[N]; ll ans; int n, m, dfstime, top, ext; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } int Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; int siz = 1; for (int v : G.e[u]) { if (!dfn[v]) { siz += Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } return siz; } void dfs(int u, int f, int Siz) { siz[u] = (u <= n); ll sum = 0; for (int v : T.e[u]) if (v != f) dfs(v, u, Siz), sum += 2ll * siz[u] * siz[v], siz[u] += siz[v]; sum += 2ll * siz[u] * (Siz - siz[u]); ans += sum * (u <= n ? -1 : T.e[u].size()); } signed main() { ext = n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) dfs(i, 0, Tarjan(i)); printf("%lld", ans); return 0; }
CF487E Tourists
给定一张简单无向连通图,点有点权 wi ,要求支持两种操作:
- 修改一个点的点权。
- 询问两点之间所有简单路径上点权的最小值。
n,m,q≤105
发现一个点双对答案的贡献为点双里面的最小权值。构建圆方树,方点的权值为点双中的最小圆点权值。然后原图就变成了一棵树,询问时就可以直接树剖套线段树求路径最小值了。
一次修改一个圆点的点权,需要修改所有和它相邻的方点,这样很容易被卡到 O(n) 个修改。树上邻域修改的一个技巧是整体维护儿子,单独维护父亲。考虑令方点权值为自己的儿子圆点的权值最小值,这样的话修改时只需要修改父亲方点。对于方点的维护,只需要对每个方点开一个 multiset
维护权值集合即可。
需要注意的是查询时若 LCA 是方点,则还需要查 LCA 的父亲圆点的权值。
时间复杂度 O(qlog2n) 。
#include <bits/stdc++.h> using namespace std; const int inf = 0x3f3f3f3f; const int N = 2e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; multiset<int> st[N]; int a[N], dfn[N], low[N], sta[N]; int n, m, q, ext, dfstime, top; template<class T = int> inline T read() { char c = getchar(); bool sign = c == '-'; while (c < '0' || c > '9') c = getchar(), sign |= c == '-'; T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline char readc() { char c = getchar(); while (c != 'A' && c != 'C') c = getchar(); return c; } namespace TCD { int fa[N], dep[N], siz[N], son[N], top[N], dfn[N], id[N]; int sum, dfstime; namespace SMT { int mn[N << 2]; inline int ls(int x) { return x << 1; } inline int rs(int x) { return x << 1 | 1; } inline void pushup(int x) { mn[x] = min(mn[ls(x)], mn[rs(x)]); } void build(int x, int l, int r) { if (l == r) { mn[x] = a[id[l]]; return; } int mid = (l + r) >> 1; build(ls(x), l, mid), build(rs(x), mid + 1, r); pushup(x); } void update(int x, int nl, int nr, int pos, int k) { if (nl == nr) { mn[x] = k; return; } int mid = (nl + nr) >> 1; if (pos <= mid) update(ls(x), nl, mid, pos, k); else update(rs(x), mid + 1, nr, pos, k); pushup(x); } int query(int x, int nl, int nr, int l, int r) { if (l <= nl && nr <= r) return mn[x]; int mid = (nl + nr) >> 1; if (r <= mid) return query(ls(x), nl, mid, l, r); else if (l > mid) return query(rs(x), mid + 1, nr, l, r); else return min(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r)); } } // namespace SMT void dfs1(int u, int f) { fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1; for (int v : T.e[u]) { if (v == f) continue; dfs1(v, u), siz[u] += siz[v]; if (siz[v] > siz[son[u]]) son[u] = v; } } void dfs2(int u, int topf) { top[u] = topf, id[dfn[u] = ++dfstime] = u; if (son[u]) dfs2(son[u], topf); for (int v : T.e[u]) if (v != fa[u] && v != son[u]) dfs2(v, v); } inline int query(int x, int y) { int res = inf; while (top[x] != top[y]) { if (dfn[top[x]] < dfn[top[y]]) swap(x, y); res = min(res, SMT::query(1, 1, ext, dfn[top[x]], dfn[x])); x = fa[top[x]]; } if (dfn[x] > dfn[y]) swap(x, y); res = min(res, SMT::query(1, 1, ext, dfn[x], dfn[y])); if (x > n) res = min(res, a[fa[x]]); return res; } } // namespace TCD void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } } signed main() { n = read(), m = read(), q = read(); for (int i = 1; i <= n; ++i) a[i] = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } ext = n; for (int i = 1; i <= n; ++i) if (!dfn[i]) Tarjan(i); TCD::dfs1(1, 0), TCD::dfs2(1, 1); for (int i = 2; i <= n; ++i) st[TCD::fa[i]].insert(a[i]); for (int i = n + 1; i <= ext; ++i) a[i] = *st[i].begin(); TCD::SMT::build(1, 1, ext); while (q--) { if (readc() == 'C') { int x = read(), k = read(); TCD::SMT::update(1, 1, ext, TCD::dfn[x], k); if (x == 1) { a[x] = k; continue; } int f = TCD::fa[x]; st[f].erase(st[f].find(a[x])), st[f].insert(a[x] = k); TCD::SMT::update(1, 1, ext, TCD::dfn[f], a[f] = *st[f].begin()); } else { int x = read(), y = read(); printf("%d\n", TCD::query(x, y)); } } return 0; }
P4606 [SDOI2018] 战略游戏
给定一张无向图,每次给出一个点集 S ,求有多少个点 u 满足 u∉S 且删掉点 u 后 S 中的点不连通。
n,q≤105
先建出圆方树,则变为询问 S 在圆方树上对应的连通子图中的不在 S 中的圆点个数。
记每个圆点和父亲方点的边权为 1 ,则问题转化为 S 在圆方树上对应的极小连通子树的边权和,答案即为 dfn 相邻点距离和的一半。
注意要答案要减去 |S| ,且若子图中的深度最浅的节点是圆点,答案还要加上 1 。
弱化版(|S|=2):P4320 道路相遇
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 7, LOGN = 21; struct Graph { vector<int> e[N]; inline void clear(int n) { for (int i = 1; i <= n; ++i) e[i].clear(); } inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; int fa[N][LOGN]; int dfn[N], low[N], sta[N], dep[N], dis[N]; int n, m, q, ext, dfstime, top; template<class T = int> inline T read() { char c = getchar(); bool sign = c == '-'; while (c < '0' || c > '9') c = getchar(), sign |= c == '-'; T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } } void dfs(int u, int f) { fa[u][0] = f, dep[u] = dep[f] + 1, dis[u] = dis[f] + (u <= n), dfn[u] = ++dfstime; for (int i = 1; i < LOGN; ++i) fa[u][i] = fa[fa[u][i - 1]][i - 1]; for (int v : T.e[u]) if (v != f) dfs(v, u); } inline int LCA(int x, int y) { if (dep[x] < dep[y]) swap(x, y); for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1) if (h & 1) x = fa[x][i]; if (x == y) return x; for (int i = LOGN - 1; ~i; --i) if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i]; return fa[x][0]; } inline int dist(int x, int y) { return dis[x] + dis[y] - dis[LCA(x, y)] * 2; } signed main() { int Task = read(); while (Task--) { n = read(), m = read(); G.clear(n); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } memset(dfn + 1, 0, sizeof(int) * n); memset(low + 1, 0, sizeof(int) * n); T.clear(n * 2), dfstime = top = 0, ext = n, Tarjan(1); dfstime = 0, dfs(1, 0); q = read(); while (q--) { vector<int> qry(read()); for (int &it : qry) it = read(); sort(qry.begin(), qry.end(), [](const int &x, const int &y) { return dfn[x] < dfn[y]; }); int ans = 0; for (int i = 0; i < qry.size(); ++i) ans += dist(qry[i], qry[(i + 1) % qry.size()]); ans = ans / 2 - qry.size(); if (LCA(qry.front(), qry.back()) <= n) ++ans; printf("%d\n", ans); } } return 0; }
CF1763F Edge Queries
给定一张无向图,q 次询问,每次给出点对 (s,t) ,求有多少条能出现在 s 到 t 简单路径上的边满足删去后 s 与 t 仍连通。
n,m,q≤2×105
构建圆方树,对于路径上的方点,若该方点表示的点双不为两点一边,则整个点双中的所有边都合法。
问题转化为树上路径求点权和,不难用树剖做到 O(qlogn) 。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 7, LOGN = 19; struct Graph { vector<int> e[N << 1]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; int fa[N << 1][LOGN]; int dfn[N], low[N], sta[N], sum[N << 1], dep[N << 1]; int n, m, q, dfstime, top, ext; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } } void dfs1(int u, int f) { fa[u][0] = f, dep[u] = dep[f] + 1; for (int i = 1; i < LOGN; ++i) fa[u][i] = fa[fa[u][i - 1]][i - 1]; for (int v : T.e[u]) if (v != f) dfs1(v, u); } void dfs2(int u) { sum[u] = (sum[u] == 2 ? 0 : sum[u] / 2) + sum[fa[u][0]]; for (int v : T.e[u]) if (v != fa[u][0]) dfs2(v); } inline int LCA(int x, int y) { if (dep[x] < dep[y]) swap(x, y); for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1) if (h & 1) x = fa[x][i]; if (x == y) return x; for (int i = LOGN - 1; ~i; --i) if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i]; return fa[x][0]; } inline int query(int x, int y) { int lca = LCA(x, y); return sum[x] + sum[y] - sum[lca] - sum[fa[lca][0]]; } signed main() { n = read(), m = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } ext = n, Tarjan(1), dfs1(1, 0); for (int u = 1; u <= n; ++u) for (int v : G.e[u]) { if (fa[fa[v][0]][0] == u) ++sum[fa[v][0]]; else ++sum[fa[u][0]]; } dfs2(1), q = read(); while (q--) { int s = read(), t = read(); printf("%d\n", query(s, t)); } return 0; }
P9167 [省选联考 2023] 城市建造
给定一张 n 个点 m 条边的无向连通图 G=(V,E),询问有多少该图的子图 G′=(V′,E′) 满足:
- E′≠∅ 。
- G−E′ 中恰有 |V′| 个连通块。
- G−E′ 中任意两个连通块大小之差不超过 k 。
答案对 998244353 取模。
3≤n≤105,n−1≤m≤2×105,0≤k≤1
由于 G−E′ 恰有 |V′| 个连通块,故 V′ 中的每个点在 G−E′ 分别属于不同的连通块,且仅通过 E′ 使得 V′ 连通。
对于选择的两个点,若存在一条简单路径连接它们,则路径上的每个点都要选择,否则这两个点在 G−E′ 中连通。
进一步的,若一个点双里面选了两个点,则整个点双都要选。
建出圆方树,称删去一个方点表示选其所有邻域圆点,则删去的方点通过圆点直接构成一个连通块。
考虑第三条限制,删去选择的方点后,每个连通块大小(圆点数量)相差不超过 k。
先考虑 k=0 的情况,则连通块大小 d 为 n 的因数。考虑枚举连通块大小 d ,统计方案数。
以带权重心 R 为根,若 R 为方点则令其相邻圆点为 R 。令圆点权值为 1 ,方点权值为 0 ,sizi 表示 i 子树内的权值和。分类讨论每个方点 u 是否被删去:
- sizu<d :不删 u 。
- sizu>d :删去 u 。
- sizu=d :若 u 只有一个儿子则删去 u ,否则不删 u 。
再判断一下该方案的合法性即可,可以在线维护 cnti 表示大小为 i 的连通块数量。
从小到大枚举 d ,用并查集维护即可做到 O(nα(n)) 。
再考虑 k=1 的情况,可以容斥,用连通块大小为 d 或 d+1 的方案数,然后减去连通块大小为 d 的方案数,再减去连通块大小为 d+1 的方案数。
对于 siz≠d 的情况,按照 k=0 的方法处理即可。
否则考虑一个圆点所有 siz=d 的方点儿子,显然这些方点最多保留一个。先钦定删去所有 siz=d 的点,此时若一个方点的父亲圆点所在连通块大小 >1 ,则其所有 siz=d 的儿子都必须删,否则只能保留任意一个。即对于所有存在 siz=d 的方点儿子的圆点,若其满足所在连通块大小为 1 ,则将方案数乘上 siz=d 的方点儿子数量。
注意在 d=1 时一个圆点的所有 siz=d=1 的方点儿子可以都删去,也可以保留一个,因为该圆点的父亲必然被删去,若不保留则儿子方点连通块大小都为 1 ,若保留一个则该点连通块大小为 2 。因此答案要乘上其 siz=1 的方点儿子的数量 +1 。
同样使用并查集维护即可做到 O(nα(n)) 。
#include <bits/stdc++.h> using namespace std; const int Mod = 998244353; const int N = 2e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G, T; struct DSU { int fa[N], siz[N], cnt[N]; inline void prework(int n) { iota(fa + 1, fa + n + 1, 1); fill(siz + 1, siz + n + 1, 1); memset(cnt + 1, 0, sizeof(int) * n); cnt[1] = n; } inline int find(int x) { while (x != fa[x]) fa[x] = fa[fa[x]], x = fa[x]; return x; } inline void merge(int x, int y) { x = find(x), y = find(y); if (x == y) return; --cnt[siz[x]], --cnt[siz[y]]; ++cnt[siz[x] += siz[y]], fa[y] = x; } } dsu; vector<int> vec[N]; int dfn[N], low[N], sta[N], siz[N], mxsiz[N], fa[N], sum[N], ans1[N]; bool ans0[N]; int n, m, k, ext, dfstime, top, root; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline int add(int x, int y) { x += y; if (x >= Mod) x -= Mod; return x; } inline int dec(int x, int y) { x -= y; if (x < 0) x += Mod; return x; } void Tarjan(int u) { dfn[u] = low[u] = ++dfstime, sta[++top] = u; for (int v : G.e[u]) { if (!dfn[v]) { Tarjan(v), low[u] = min(low[u], low[v]); if (low[v] == dfn[u]) { T.insert(++ext, u), T.insert(u, ext); while (sta[top] != v) T.insert(ext, sta[top]), T.insert(sta[top--], ext); T.insert(ext, sta[top]), T.insert(sta[top--], ext); } } else low[u] = min(low[u], dfn[v]); } } void getroot(int u, int f, int Siz) { siz[u] = (u <= n), mxsiz[u] = 0; for (int v : T.e[u]) if (v != f) getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]); mxsiz[u] = max(mxsiz[u], Siz - siz[u]); if (!root || mxsiz[u] < mxsiz[root]) root = u; } void dfs(int u, int f) { fa[u] = f, siz[u] = (u <= n); for (int v : T.e[u]) if (v != f) dfs(v, u), siz[u] += siz[v]; } signed main() { n = read(), m = read(), k = read(); for (int i = 1; i <= m; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } ext = n, Tarjan(1); getroot(1, 0, n); if (root > n) root = T.e[root][0]; dfs(root, 0); for (int i = n + 1; i <= ext; ++i) vec[siz[i]].emplace_back(i); dsu.prework(n); for (int i = 1; i < n; ++i) { for (int u : vec[i]) if (T.e[u].size() > 2) for (int j = 1; j < T.e[u].size(); ++j) dsu.merge(T.e[u][0], T.e[u][j]); ans0[i] = (dsu.cnt[i] * i == n); vector<int> now; for (int u : vec[i]) { if (T.e[u].size() > 2) continue; if (dsu.siz[dsu.find(fa[u])] == 1) { dsu.merge(fa[u], T.e[u][T.e[u][0] == fa[u]]); now.emplace_back(fa[u]), sum[fa[u]] = 1; } else if (sum[fa[u]]) ++sum[fa[u]]; } if (dsu.cnt[i] * i + dsu.cnt[i + 1] * (i + 1) == n) { ans1[i] = 1; for (int u : now) ans1[i] = 1ll * ans1[i] * (sum[u] + (i == 1)) % Mod; } for (int u : vec[i]) if (T.e[u].size() == 2) for (int j = 1; j < T.e[u].size(); ++j) dsu.merge(T.e[u][0], T.e[u][j]); for (int u : now) sum[u] = 0; } if (!k) return printf("%d", count(ans0 + 1, ans0 + n, true)), 0; int ans = 0; for (int i = 1; i < n; ++i) ans = add(ans, ans1[i]); printf("%d", dec(ans, count(ans0 + 1, ans0 + n, true))); return 0; }
本文作者:wshcl
本文链接:https://www.cnblogs.com/wshcl/p/18306858/connectivity
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步