給出一張 \(n\) 個點 \(m\) 條邊的無向圖,可能不連通、有重邊、有自環、有割邊。求其所有極大的邊三連通分量。
\(n, m \le 5 \times 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,\text{ such that } f = (u, z) \text{ for some } z \in V' - \{w\} \}\)。而 \(V''\) 需要分類討論,若 \(deg_{G'}(u) = 2\),則 \(u\) 會被 \(w\) 吐出來,那么 \(V'\) 沒變;若 \(deg_{G'}(u) \neq 2\),則 \(u\) 被 \(w\) 吸收,\(V'' = V' - \{u\}\),\(\sigma(w) = \sigma(w) \cup \sigma(u)\)。
由於可以證明(第一個待補證明的坑),若 \(deg_{G'}(u) \neq 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 \text{ is a child of } w\} \cup \{ dfn(w') \mid (w, w') \text{ 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 \to low(u) \to low(w) \to 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 \to low(u) \to low(w) \to 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\n", (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], " \n"[j == s - 1]);
}
}
int main() {
init();
solve();
return 0;
}