给出一张 (n) 个点 (m) 条边的无向图,可能不连通、有重边、有自环、有割边。求其所有极大的边三连通分量。
(n, m le 5 imes 10 ^ 5)。
论文太长了,还没看完,目前只看懂了算法步骤,一些证明还咕在后面。就先介绍一下步骤,正确性证明和时间复杂度证明等我看懂以后补上来。附一个论文原地址:A Simple 3-Edge-Connected Component Algorithm,来源选 ResearchGate 那个可以免费下载。本文内图片均出自这篇论文。
由于这个算法的核心在于其中的 Absort-Eject 操作,我习惯称其为 Absorb-Eject 算法。Absorb-Eject 算法的思想与求点双、边双的 Tarjan 算法类似,都是利用算法过程中建出的 dfs 树,求出点之间的连边情况。故为了更清晰地弄懂这个算法,最好对点双、边双的 Tarjan 算法有一定的理解。
为了减少讨论,我们需要先删除掉原图上一些可有可无,但会导致一些麻烦的分类情况的边:自环和割边。
- 自环:显然存在一个最优方案使得连通的三条路径都不包含自环,故自环可删。
- 边三连通分量一定是边双连通分量,因此割边两端的边不可能属于同一个边三连通分量,故割边可删。
经过这样预处理转化后,我们将原图变成了若干无自环的边双连通分量的连通块。那么以下的算法过程,均在这样的边双中进行。
首先,对限制条件进行一定的观察:两个点 (u, v) 在相同的边三内,当且仅当不存在一个边对 ((e_1, e_2)),满足将原图的 (e_1, e_2) 割开以后,(u) 与 (v) 不连通。
再加上这张图内没有割边,我们可以定义一个类似割边的定义:切边。我们称一条边 (e) 是切边,当且仅当它能够与另外一条边 (e') 配合,把原图割成两个连通块。那么,对于一条边 (e = (u, v)),若 (e) 是一条切边,则 (u, v) 一定不在一个相同的边三内;若 (e) 不是一条切边,则 (u, v) 一定在一个相同的边三内。所以我们只需要把原图中所有切边删去,剩下的边就将原图连成了若干边三。
于是我们明确了算法的目的:确定每条边是否为切边。
这个算法的核心步骤是 Absorb-Eject 操作,可译为吞吐操作。Absorb 会在一条边 ((w, u)) 上进行,表示 (w) 将 (u) 吞并。吞并时,(u) 消失,所有与 (u) 相邻的边 ((x, u))(除了 ((w, u)) 以外),都变成与 (w) 相邻的边 ((x, w))。特殊地,如果 (u) 的点度为 (2) (注意此时的点度是吞并后形成的新图的点度,而点 (u) 也可能已吞并了若干个点),那么可以割开这两条边使得 (u) 与外界不连通,说明 (u) 及 (u) 已吞并过的点是一个单独的边三,就让 (w) 将 (u) 吐出来,而吐出来的 (u) 失去所有相邻的边。
形式化来讲,对于每个点 (u),定义其已吞并点集为 (sigma(u)),初始时,(sigma(u) = {u})。进行到目前的图为 (G' = (V', E')),进行吞吐的边为 ((w, u))。那么进行一次 Absorb-Eject 操作后,图会变成 (G' / e = (V'', E''))。其中 (E'' = E' setminus E_u cup E_{w ^ +}),其中 (E_u) 表示 (G') 中与 (u) 相邻的边,(E_{w ^ +} = { f' = (w, z) mid exists f in E_u, ext{ such that } f = (u, z) ext{ for some } z in V' - {w} })。而 (V'') 需要分类讨论,若 (deg_{G'}(u) = 2),则 (u) 会被 (w) 吐出来,那么 (V') 没变;若 (deg_{G'}(u) eq 2),则 (u) 被 (w) 吸收,(V'' = V' - {u}),(sigma(w) = sigma(w) cup sigma(u))。
由于可以证明(第一个待补证明的坑),若 (deg_{G'}(u) eq 2),则 ((w, u)) 一定不是切边,也就是 (w, u) 一定在一个边三内。换句话说,就是 (sigma(w)) 就是 (w) 所代表的一个原图上的一个边三。在进行若干次吞并后,所有的边都消失了,变成若干独立的点。则每个独立的点就代表着原图上一个极大边三连通分量,就是我们想求的东西。
以上是核心步骤 Absorb-Eject。我们接下来用一个类似 Tarjan 算法的 dfs 过程,配合着 Absorb 操作,将原图一步步变成这样没有边的图,得到每一个表示极大边三连通分量的独立点。
又有一个奇怪的结论(第二个待补证明的坑):递归完一个子树 (u) 结束回溯后,子树 (u) 内所有仍未确定是否为切边的边形成了一条一端为 (u) 的路径,也即修改后的图形成了一条一端为 (u) 的路径和若干代表者边三连通分量的独立点。我们称 (u) 上挂着的这条路径为 (u) - path,记 (P_u),我们需要在 dfs 的过程中维护 (P_u),最终到达根 (r) 时的 (P_r) 会为空,也就是再没有未确定是否为切边的边,就结束了我们的算法过程。
dfs 过程中,同样记录 (low) 和 (dfn),(dfn(u)) 表示点 (u) 在 dfs 序中的编号,(low(u)) 表示 (u) 经过最多一条返祖边能到达的 (dfn) 最小值,那么有 (low(w) = min({low(u) mid u ext{ is a child of } w} cup { dfn(w') mid (w, w') ext{ is a back-edge} } cup {dfn(w)}))。我们令此时 dfs 到了一个点 (w),枚举其相邻边,分类讨论更新 (low) 和 (P_w)。
- ((w, u)) 是一条没用的边,即 (w = u),或 ((w, u)) 为割边,或 (u) 是 (w) 的父亲且 (w) 是从 (u) 的这条边过来(就是父边)。不管,continue。
- ((w, u)) 是一条树边。递归执行 (dfs(u))。首先判断一下 (deg_{G'}(u)) 是否为 (2),如果等于 (2) 那么要先把 (u) 独立吐出来形成一个单独的边三,同时把 (u) 从 (P_u) 中去掉,(P_u = P_u - u)。接着看 (low(u)) 是否会对 (low(w)) 产生贡献:
- 若 (low(u) < low(w)),大概由于增加了一条 (u o low(u) o low(w) o P_w) 的路径,原本还未确定的 (P_w) 可以确定为不是切边了,于是让 (w) 将原本的 (P_w) 吞并掉,然后用 (w + P_u) 把 (P_w) 替换掉。
- 若 (low(u) ge low(w)),类似上一条,原本还未确定的 (P_u) 可以确定为不是切边了,让 (w) 把 (P_u) 吞并掉,保持 (P_w) 不变。
- 若 (low(u) < low(w)),大概由于增加了一条 (u o low(u) o low(w) o P_w) 的路径,原本还未确定的 (P_w) 可以确定为不是切边了,于是让 (w) 将原本的 (P_w) 吞并掉,然后用 (w + P_u) 把 (P_w) 替换掉。
- ((w, u)) 是一条返祖边。若再满足 (dfn(u)) 可以更新 (low(w)),那么 (P_w) 可以确定为不是切边了,这时让 (w) 把 (P_w) 吞并掉,然后 (P_w) 清空。
- ((w, u)) 是一条前向边。由于 ((w, u)) 这条边的存在,(u) 一定落在 (P_w) 上。那么这时 (P_w) 的 ([w cdots u]) 部分可以确定为不是切边了,就让 (u) 把 (P_w) 的 ([w cdots u]) 部分吞并掉,剪掉 (P_w) 的这段前缀。
由于 (low(r) = 1),所有的树边都会到 (low(u) ge low(w)) 这条,因此 (P_r) 保持为空。也就是上面所说的,递归到根结束后,就确定了每条边是否为切边,算法顺利完成。
贴上论文中给出的伪代码:
最后,注意到图变化的时候边不需要显式地维护,只要维护每个点的相邻点度就好了。代码能比较容易地写出来。
我用了并查集维护一个点的集合,所以时间复杂度 (mathcal{O}((n + m) log n))。实现细致一点可以把并查集扔掉,时间复杂度为 (mathcal{O}(n + m))。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <utility>
#include <vector>
const int MaxN = 500000, MaxM = 500000;
struct graph_t {
int cnte;
int head[MaxN + 5], to[MaxM * 2 + 5], next[MaxM * 2 + 5];
graph_t() { cnte = 1; }
inline void addEdge(int u, int v) {
cnte++; to[cnte] = v;
next[cnte] = head[u]; head[u] = cnte;
}
};
struct union_find {
int par[MaxN + 5];
union_find() { memset(par, -1, sizeof par); }
int find(int x) { return par[x] < 0 ? x : par[x] = find(par[x]); }
inline void merge(int u, int v) {
int p = find(u), q = find(v);
if (p == q) return;
par[p] += par[q];
par[q] = p;
}
};
int N, M;
graph_t Gr;
class two_edge_connect {
private:
int low[MaxN + 5], dfn[MaxN + 5], dfc;
int stk[MaxN + 5], tp;
int bel[MaxN + 5], s;
void dfs(int u, int fe) {
low[u] = dfn[u] = ++dfc;
stk[++tp] = u;
for (int i = Gr.head[u]; i; i = Gr.next[i]) {
if ((i ^ fe) == 1) continue;
int v = Gr.to[i];
if (dfn[v] == 0) {
dfs(v, i);
low[u] = std::min(low[u], low[v]);
} else
low[u] = std::min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
s++;
for (;;) {
int v = stk[tp--];
bel[v] = s;
if (u == v) break;
}
}
}
public:
void init() {
memset(dfn, 0, sizeof dfn);
dfc = tp = s = 0;
for (int i = 1; i <= N; ++i)
if (dfn[i] == 0) dfs(i, 0);
}
inline bool isbridge(int u, int v) {
return bel[u] != bel[v];
}
};
class three_edge_connect {
private:
two_edge_connect bcc;
union_find uf;
int low[MaxN + 5], dfn[MaxN + 5], end[MaxN + 5], dfc;
int deg[MaxN + 5];
inline bool insubtree(int u, int v) {
if (dfn[u] <= dfn[v] && dfn[v] <= end[u]) return true;
else return false;
}
inline void absorb(std::vector<int> &path, int u, int w = 0) {
while (path.empty() == false) {
int v = path.back();
if (w > 0 && insubtree(v, w) == false) break;
path.pop_back();
deg[u] += deg[v] - 2;
uf.merge(u, v);
}
}
void dfs(int u, int fe, std::vector<int> &pu) {
low[u] = dfn[u] = ++dfc;
for (int i = Gr.head[u]; i; i = Gr.next[i]) {
int v = Gr.to[i];
if (u == v || bcc.isbridge(u, v) == true) continue;
deg[u]++;
if ((i ^ fe) == 1) continue;
if (dfn[v] == 0) {
std::vector<int> pv;
dfs(v, i, pv);
if (deg[v] == 2) pv.pop_back();
if (low[v] < low[u]) {
low[u] = low[v];
absorb(pu, u);
pu = pv;
} else absorb(pv, u);
} else {
if (dfn[v] > dfn[u]) {
absorb(pu, u, v);
deg[u] -= 2;
} else if (dfn[v] < low[u]) {
low[u] = dfn[v];
absorb(pu, u);
}
}
}
end[u] = dfc;
pu.push_back(u);
}
public:
void init() {
memset(dfn, 0, sizeof dfn);
memset(deg, 0, sizeof deg);
dfc = 0;
bcc.init();
for (int i = 1; i <= N; ++i) {
if (dfn[i] == 0) {
std::vector<int> pi;
dfs(i, 0, pi);
}
}
}
std::vector< std::vector<int> > getall() {
std::vector< std::vector<int> > res(N), ans;
for (int i = 1; i <= N; ++i) {
int x = uf.find(i);
res[x - 1].push_back(i);
}
for (int i = 0; i < N; ++i)
if (res[i].empty() == false) ans.push_back(res[i]);
return ans;
}
};
void init() {
scanf("%d %d", &N, &M);
for (int i = 1; i <= M; ++i) {
int u, v;
scanf("%d %d", &u, &v);
Gr.addEdge(u, v);
Gr.addEdge(v, u);
}
}
inline bool cmp(const std::vector<int> &x, const std::vector<int> &y) { return x[0] < y[0]; }
void solve() {
static three_edge_connect tcc;
tcc.init();
std::vector< std::vector<int> > ans = tcc.getall();
for (int i = 0; i < (int) ans.size(); ++i)
std::sort(ans[i].begin(), ans[i].end());
std::sort(ans.begin(), ans.end(), cmp);
printf("%d
", (int) ans.size());
for (int i = 0; i < (int) ans.size(); ++i) {
int s = (int) ans[i].size();
for (int j = 0; j < s; ++j)
printf("%d%c", ans[i][j], "
"[j == s - 1]);
}
}
int main() {
init();
solve();
return 0;
}