邊三連通分量算法


題目鏈接

給出一張 \(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\) 所代表的一個原圖上的一個邊三。在進行若干次吞並后,所有的邊都消失了,變成若干獨立的點。則每個獨立的點就代表着原圖上一個極大邊三連通分量,就是我們想求的東西。

pic1.png

以上是核心步驟 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\) 替換掉。
      pic2.png
    • \(low(u) \ge low(w)\),類似上一條,原本還未確定的 \(P_u\) 可以確定為不是切邊了,讓 \(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\) 的這段前綴。
    pic3.png

由於 \(low(r) = 1\),所有的樹邊都會到 \(low(u) \ge low(w)\) 這條,因此 \(P_r\) 保持為空。也就是上面所說的,遞歸到根結束后,就確定了每條邊是否為切邊,算法順利完成。

貼上論文中給出的偽代碼:

pic4.png

最后,注意到圖變化的時候邊不需要顯式地維護,只要維護每個點的相鄰點度就好了。代碼能比較容易地寫出來。

我用了並查集維護一個點的集合,所以時間復雜度 \(\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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM