Graph[2]

algorithm[18] 22527 Brine

我是誰

22527 鄭竣陽
Brine
BrineTW#7355
  • 你是不是忘記我是誰了ㄏㄏ
Index
Connectivity

連通性

連通性

  • 如何判斷一張圖是否連通
    • 簡單的 BFS / DFS 都能處理
  • 但除了連通與否,連通的強度也是一大重點

連通性

  • 如何判斷一張圖是否連通
    • 簡單的 BFS / DFS 都能處理
  • 但除了連通與否,連通的強度也是一大重點
  • 如果中間的關鍵點沒了就不連通了
    • 越高的容錯率越好
  • 點連通度
    • 最少要拔掉多少點使得圖不連通
  • 邊連通度
    • 最少要拔掉多少邊使得圖不連通

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 \(\ge 2\) 的網路
  • 但有時候就是會遇到連通度只有 \(1\) 的網路
    • 每個點或每個邊被拔掉都會使圖不連通嗎?
    • 誰被拔掉會使圖不連通?

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通
  • 又被稱為關節點 articulation vertex
  • 要怎麼求出所有的割點呢?

暴力作法

  • 要怎麼求出一個點 \(v\) 是不是割點呢?
    • 根據定義,把他去掉後圖片不連通的話,該點即為割點
    • 那我們就照定義做
      • 選一個點 \(v\),拔除之
      • 對圖做一次 DFS,判斷沒有經過 \(v\) 能否遍歷整張圖
      • 對所有點做以上操作
    • \(\mathcal O(|V|)\) 窮舉點,\(\mathcal O(|V| + |E|)\) DFS
      • 總複雜度 \(\mathcal O(|V|^2 + |V||E|)\)

DFS Tree

  • 在討論連通度時,我們有一個強大的工具,DFS Tree(DFST)
  • 在 DFS 走過所有能走的點時,碰到的邊可以分為以下幾種:
    • 樹邊(tree edge)
      • DFS 時候碰到新的點用的邊
      • 從祖先連到其子代
    • 返祖邊(back edge):從子孫連到祖先
    • 前向邊(forward edge):從祖先連到非子代的子孫
    • 交錯邊(cross edge):跨越兩顆樹的邊

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧
  • 那如果換成無向圖呢?

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧
  • 那如果換成無向圖呢?
    • 無向圖只有樹邊跟返祖邊,我們先來做無向圖吧

如何判斷誰是割點

  • 可以看得出來,這些點是割點

如何判斷誰是割點

  • 這樣好像看不出來什麼性質,移動一下好了
  • 可以看得出來,這些點是割點

如何判斷誰是割點

  • 一棵樹中除了葉都是割點
    • 拔除 \(v\) 使圖被分成 \(\deg(v)\) 塊
  • 在 DFS 樹中呢?
    • 若 \(v\) 的任意子孫不能連回祖先
    • 沒有返祖邊

DFS 樹的根節點 \(r\)

  • 只要子孫都能連回祖先,自己就不是割點
  • 對於 \(r\),自己沒有祖先可言
    • 子孫不可能連回祖先
  • 只要 \(\deg(r) > 1\),\(r\) 即為割點

利用割點性質

  • 只要符合剛才性質的,就是割點
  • 那我們照定義,對每個節點的子孫(\(\mathcal O(|V|)\))
    • 嘗試不經過該節點回到該節點的祖先(\(\mathcal O(|V| + |E|)\))
    • 單點複雜度 \(\mathcal O(|V|^2 + |V||E|)\)
      • 比剛剛還糟糕
      • 勢必得要加速演算法不然我教幹嘛

一位傳奇人物

  • Robert Endre Tarjan
    • 1986 圖靈獎
  • 貢獻
    • 並查集
    • 離線 LCA 演算法
    • 連通性演算法
    • splay tree
    • Fibonacci heap
  • 你今晚的夢魘

Tarjan 的想法

  • 剛剛我們的問題有:
    • 每次判定子節點連回祖先太久
    • 每個節點的子節點太多
  • 所以他就想到我們可以:
    • 定義一個函數 \(\text{low}(v)\) 回傳 \(v\) 不經過親代能走到的最低深度
    • 只要 \(\text{low}(v) < \text{depth}(u)\) 就代表 \(v\) 不通過 \(u\) 也能走回祖先
    • 如果維護成功,就可以均攤複雜度
  • 除此之外,在無向圖中:
    • \(u\) 的子代 \(v\) 的子樹只需存在一點 \(w\) 使得 \(\text{low}(w) < \text{depth}(u)\)
    • 只要查詢 \(v\) 就好,均攤只要 \(\mathcal O(1)\)

如何計算 \(\text{low}(v)\)

  • 一個點 \(v\) 如果能不靠祖先連到深度為 \(d\) 的點,代表:
    • 自己能有邊能連到深度為 \(d\) 的點
    • 自己的子孫能連到深度為 \(d\) 的點
  • ​\(\text{low}(v)\) 能從子代轉移!
    • ​DFS 時記錄然後遞迴完子代後比較就好!
    • 複雜度只有 \(\mathcal O(|V| + |E|)\)!

Tarjan's Cut Vertex

  • 記錄深度太麻煩了,不如我們記錄 DFS 入點時的順序吧!
vector<int> low(vC);
vector<int> preorder(vC);
vector<bool> isCutVertex(vC);

int counter = 0;
function<void(int, int)> dfs = [&](int u, int last) {
    low[u] = preorder[u] = ++counter;

    int child = 0;

    for (auto& v: graph[u]) {
        if (v == last) continue;

        if (preorder[v]) { // is back edge
            low[u] = min(low[u], preorder[v]);
        } else {
            dfs(v, u);

            if (last >= 0 && low[v] >= preorder[u]) {
                isCutVertex[u] = true;
            }

            child++;
            low[u] = min(low[u], low[v]);
        }
    }

    if (last < 0 && child > 1) isCutVertex[u] = true;
};

dfs(0, -1);

為什麼 \(\text{order(i)}\) 是好的

  • 相信他就好你管那麼多
  • 在建 DFS Tree 的時候,有以下兩種情況:
    • 往下遍歷時,碰到還沒有走訪過的點
      • 一般的樹邊
      • 連到的全部都是自己的子代
      • 該點的前序 (進入戳記) 一定大於自己
    • 往下遍歷時,碰到已經被走訪過的點
      • 一定是連到自己直系祖先的返祖邊
        • 如果連到不是自己的直系祖先?

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊
  • 跟割點有什麼關係?

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊
  • 跟割點有什麼關係?
  • 還是不知道?

還是 Tarjan 的想法

  • 我們把剛才那張圖變成它的 DFST
  • 我們把剛才那張圖變成它的 DFST

還是 Tarjan 的想法

還是 Tarjan 的想法

  • 我們把剛才那張圖變成它的 DFST
  • 對於一個點 \(v\)
    • 若其子樹裡沒有返祖邊能連到 \(v\) 的祖先 \(u\) 或以上
    • 則 \((u, v)\) 是一座橋
  • 一樣用 \(\text{low}(v)\) 函數處理
    • 改成 \(\text{low}(v) \le \text{order}(u)\)

Tarjan's Bridge

  • 重邊需要另外看,如果不是簡單圖要記得喔
vector<int> low(vC);
vector<int> order(vC);
set<pii> isBridge;

int counter = 0;
function<void(int, int)> dfs = [&](int current, int last) {
    low[current] = order[current] = ++counter;

    for (auto& v: graph[current]) {
        if (v == last) continue;

        if (order[v]) { // is back edge
            low[current] = min(low[current], order[v]);
            // a back edge always forms a cycle
        } else {
            dfs(v, current);
            low[current] = min(low[current], low[v]);

            if (low[v] > order[current]) {
                isBridge.insert({current, v});
            }
        }
    }
};

dfs(0, -1);

例題

Components

連通分量

連通分量的定義

  • 若在一張圖 \(G\) 上存在一個連通子圖 \(G'\),則 \(G'\) 為 \(G\) 的連通分量

連通分量的定義

  • 若在一張圖 \(G\) 上存在一個連通子圖 \(G'\),則 \(G'\) 為 \(G\) 的連通分量

連通分量的定義

  • 若在一張圖 \(G\) 上存在一個連通子圖 \(G'\),則 \(G'\) 為 \(G\) 的連通分量

連通分量的定義

  • 若在一張圖 \(G\) 上存在一個連通子圖 \(G'\),則 \(G'\) 為 \(G\) 的連通分量

連通分量的定義

  • 若在一張圖 \(G\) 上存在一個連通子圖 \(G'\),則 \(G'\) 為 \(G\) 的連通分量
  • 若連通分量無法再連到其他點,則此連通分量極大(maximal)
  • 通常我們預設討論的連通分量皆為極大連通分量

雙連通分量

  • 我們剛剛在把圖上的割點和橋找出來了
  • 現在我們可以把圖分隔成若干個區塊,中間以割點或橋相隔
  • 這些區塊被稱為雙連通分量(biconnected components, BCC)
    • 有些時候是:
      • 點雙連通分量稱為 biconnected components
        • 拔掉任何邊、點仍連通的連通分量
      • 邊雙連通分量稱為 bridge connected components
        • 拔掉任何邊仍連通的連通分量
    • 或是 2-vertex / edge-connected components
  • 雙連通分量有什麼用?

把圖「縮起來」

  • 邊雙連通分量

把圖「縮起來」

  • 邊雙連通分量
8
1
3
2
7
6
5
0
4
9

把圖「縮起來」

  • 邊雙連通分量
8
1
3
2
7
6
5
0
4
9

把圖「縮起來」

  • 邊雙連通分量
8
1
3
2
7
6
5
0
4
9

把圖「縮起來」

  • 邊雙連通分量
6
5
0
1239
487

把圖「縮起來」

  • 點雙連通分量

把圖「縮起來」

  • 點雙連通分量
8
1
3
2
7
6
5
4
9
0

把圖「縮起來」

  • 點雙連通分量
8
1
3
2
7
6
5
4
9
0

把圖「縮起來」

  • 點雙連通分量
8
1
3
2
7
6
5
0
4
9

把圖「縮起來」

  • 點雙連通分量
8
1
3
2
7
6
5
0
4
9

把圖「縮起來」

  • 點雙連通分量
  • 看起來沒有縮欸
2
5
4
9
1239
0
6
487

縮完點之後可以幹嘛

  • 有的時候我們不會喜歡一張有環的圖,複雜的圖很難被處理
  • 但是如果今天要處理的是一棵樹呢?
    • 只要把任何一張圖縮起來,他就會變成一棵樹(或森林)!
  • 我們可以透過找出連通分量來使問題變得可做
    • 要用割點或橋來分割就看題目要求
  • 要怎麼找連通分量呢?

邊雙連通分量

  • 找邊雙連通分量要做的事情就是不能走橋
  • 那我們就把橋都標記起來,然後 DFS
    • DFS 的時候若該邊是橋就不往下走
  • 成功了欸
    • 標記然後 DFS 應該不用教吧
  • 那點雙連通分量也是一樣嗎?

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
1
3
2
5
0
4

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
2
4
1234
0
5

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
  • 點雙連通分量不能輕鬆把點標記成不可通行
    • 因為一個點可以存在於兩個點雙連通分量
      • 誰?
      • 割點!
  • 這樣的話還能用跟剛才類似的方法嗎?
    • 可以!
    • 但很麻煩
    • 既然這樣,我們不如來想想別的作法吧

人生而懶惰

  • 我們做了一次 DFS,把 DFS 樹找出來
  • 接著我們對某些點 DFS,算出所有點雙連通分量
    • 可是做兩次 DFS 好麻煩,要寫不一樣的函式
    • 也許只要做一次?
  • 我們其實可以只做一次 DFS 來求得所有的點雙連通分量!
    • 在 DFS 離開一個點時做一點判定
    • 若該點是割點,把其子孫中沒有 BCC 的邊指派一個 BCC

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

Vertex BCC Code

struct Edge {
    int a, b;

    Edge(int u = -1, int v = -1): a(u), b(v) {}
};

vector< vector<int> > graph;
vector<int> low, preorder;
vector<int> isCutVertex;
stack<Edge> s;

vector<int> bccId;
vector< vector<int> > bcc;

int counter = 0;
int bccCounter = 0;

void dfs(int u, int p) {
    low[u] = preorder[u] = ++counter;
    int child = 0;

    for (auto& v: graph[u]) {
        Edge e(u, v);

        if (v == p) continue;

        s.push(e);

        if (preorder[v]) {
            low[u] = min(low[u], preorder[v]);
            continue;
        }

        child++;
        dfs(v, u);
        low[u] = min(low[u], low[v]);

        if (low[v] >= preorder[u]) {
            isCutVertex[u] = 1;
            ++bccCounter;
            Edge temp;

            do {
                temp = s.top(), s.pop();

                if (bccId[temp.a] != bccCounter) bcc[bccCounter].push_back(temp.a);
                if (bccId[temp.b] != bccCounter) bcc[bccCounter].push_back(temp.b);

                bccId[temp.a] = bccId[temp.b] = bccCounter;
            } while (temp.a != u || temp.b != v);
        }

    }

    if (p <= 0 && child == 1) {
        isCutVertex[u] = 0;
    }
}

怎麼縮圖?

  • 其實直接照定義縮可能會長出很麻煩的東西
  • 有個資料結構叫做封鎖割樹圓方樹(block-cut tree)
    • 把同一個點雙連通分量改成:
      • 所有分量內的邊消失
      • 全部連到一個虛點(自己創的點),也就是方點
    • 這樣的話就一樣有樹的性質了
    • 看要求再把資訊放在圓點或方點上
  • 自己研究,我懶得放 code

回到邊雙連通分量

  • 剛剛我們討論完點雙連通分量一次求完的方法
  • 會發現其實邊雙連通分量也是一樣的概念
  • 而且其實邊雙連通比較好求?
  • 把 stack 裡面存的東西從邊換成點,判割點換成判定橋

Edge BCC Code

vector< vector<int> > graph;
vector<int> low, preorder;
stack<int> s;

vector<int> bccId;
vector< vector<int> > bcc;

int counter = 0;
int bccCounter = 0;

void dfs(int u, int p) {
    low[u] = preorder[u] = ++counter;
    s.push(u);

    for (auto& v: graph[u]) {
        if (v == p) continue;

        if (preorder[v]) {
            low[u] = min(low[u], preorder[v]);
            continue;
        }

        dfs(v, u);
        low[u] = min(low[u], low[v]);

        if (low[v] > preorder[u]) {
            ++bccCounter;
            int w;

            do {
                w = s.top(), s.pop();
                bccId[w] = bccCounter;
            } while (w != u);
        }

    }

    if (p < 0) {
        ++bccCounter;
        int w;
        while (!s.empty()) {
            w = s.top(), s.pop();
            bccId[w] = bccCounter;
        }
    }
}

有向圖上的連通分量

  • 我們終於要來講我們逃離很久的有向圖了!
  • 我們在討論有向圖的連通性的時候,會討論以下兩種分量
    • 強連通分量(strongly connected component)
      • 對於任意兩點 \(u, v\),存在 \(u \rightarrow v\) 的路徑
      • 注意不用「拔掉一個點或邊仍然成立」,不用雙連通
    • 弱連通分量(weakly connected component)
      • 對於任意兩點 \(u, v\),存在 \(u \rightarrow v\) \(v \rightarrow u\) 的路徑
      • 注意弱連通分量的定義和弱連通圖不太一樣
        • ​為什麼?
        • 我的解釋:「這種定義應用比較多」

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態
    • 縮點之後變成一張有向無環圖(DAG)!
    • 只要是 DAG 就可以拓樸排序了

Tarjan's SCC

  • 對,還是他,每次都是他
  • 應該知道要用什麼了吧
    • 沒錯!就是 DFS 樹
    • 但是現在,我們要面對 DFS 樹的每一種邊了!
      • 樹邊
      • 返祖邊
      • 前向邊
      • 交錯邊
    • 怎麼辦?
      • 不怎麼辦,就正常處理多判一點條件就好了

樹邊 vs. 前向邊

  • 戳到樹邊就是一定代表進入點時為首次入點嗎?
    • 不是,有可能前向邊先戳到了
      • 那怎麼辦?
      • 不怎麼辦!
    • 若 \(u\) 先被樹邊戳到,\(\text{low}(u)\) 已經轉移了
      • 就把它用 visited 判定,然後正常來就好
    • \(u\) 先被前向邊戳到呢?
      • ​在樹邊再次戳到時,一樣轉移就好
    • ​樹邊和前向邊不會產生影響!

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

Tarjan's SCC Code

  • 用 scc[i] 判斷交錯邊是否要更新
#include <bits/stdc++.h>

using namespace std;

vector<int> tarjan(vector< vector<int> >& graph) {
    vector<int> preorder(graph.size());
    vector<int> low(graph.size());
    vector<int> scc(graph.size());
    stack<int> s;

    int counter = 0;
    int sccId = 0;
    function<void(int)> dfs = [&](int u) {
        preorder[u] = low[u] = ++counter;
        s.push(u);

        for (auto& v: graph[u]) {
            if (scc[v]) continue;

            if (preorder[v]) low[u] = min(low[u], preorder[v]);
            if (!preorder[v]) {
                dfs(v);
                low[u] = min(low[u], low[v]);
            }
        }

        if (low[u] == preorder[u]) {
            ++sccId;
            int v;
            do {
                v = s.top(), s.pop();
                scc[v] = sccId;
            } while (u != v);
        }
    };

    for (int i = 0; i < graph.size(); i++) if (!preorder[i]) dfs(i);

    return scc;
}

另一位連通超人

  • Sambasiva Rao Kosaraju
  • 這個人想到了另外一種算強連通的方法
    • 太神奇了,他不是 Tarjan 欸
    • 但是他沒有發表該論文
    • 但他不是 Tarjan 欸
  • 讓我們來看看他怎麼想的吧!

把邊倒過來的圖

  • 在談 Kosaraju 的算法的時候,就要先談一個重要的東西
    • 反圖(transpose / converse / reverse graph)
      • 對於一個圖 \(G(V, E)\),若有一個圖:
        • 和它有相同的點集
        • 所有的邊 \((u, v)\) 都有能一一對應的邊 \((v, u)\)
      • 則稱這張圖為 \(G\) 的反圖,記為 \(G'\) 或 \(G^T, G^R \cdots\)
    • 反圖可以拿來幹嘛?

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧
    • 反圖和原圖具有相同的 SCC!
    • 若在原圖中,存在一條 \(u\) 到 \(v\) 的路徑
      • 反圖也存在一條 \(u\) 到 \(v\) 的路徑 \(\leftrightarrow\) \(u, v\) 在同一個 SCC 中

Kosaraju's SCC

  • 對一張有向圖做「偽」拓樸排序
    • 也就是「將圖 DFS 一次,並記錄其後序(離開戳記)」
  • 依照偽拓樸排序的結果,將點由後序大到小在反圖上 DFS
    • 給所有當前點開始走沒有 SCC 的點同一個新的 SCC
  • 做完了!
    • 真的假的?
    • 對,就這樣,超級好寫
    • 但是時間執行起來比 Tarjan 稍微久了一點點
  • 對於沒辦法馬上了解 Tarjan 的人,學習 Kosaraju 比較方便
  • 甚至有些人只記 Kosaraju's SCC,確保自己會 SCC

Kosaraju's SCC Code

  • 就跟你說了,短的跟鬼一樣
void dfs(int current, vector<int>& postorder, vector<bool>& visited, vector< vector<int> >& graph) {
    visited[current] = 1;

    for (auto& v: graph[current]) {
        if (visited[v]) continue;
        dfs(v, postorder, visited, graph);
    }

    postorder.push_back(current);
}

void sfd(int current, vector<int>& scc, const int id, vector< vector<int> >& hparg) {
    scc[current] = id;

    for (auto& v: hparg[current]) {
        if (scc[v]) continue;
        sfd(v, scc, id, hparg);
    }
}

把東西包成函式處理

  • 你會覺得比較長是因為我把建反圖放在裡面
vector<int> kosaraju(vector< vector<int> >& graph) {
    vector<bool> visited(graph.size());
    vector<int> postorder;
    postorder.reserve(graph.size());

    function<void(int)> dfs = [&](int u) {
        if (visited[u]) return;

        for (auto& v: graph[u]) {
            visited[v] = true;
            dfs(v);
        }

        postorder.push_back(u);
    };

    for (int u = 0; u < graph.size(); u++) dfs(u);


    vector< vector<int> > hparg(graph.size());
    for (int u = 0; u < graph.size(); u++) {
        for (auto& v: graph[u]) hparg[v].push_back(u);
    }

    vector<int> scc(graph.size());
    int sccCounter = 0;

    function<void(int)> sfd = [&](int u) {
        scc[u] = sccCounter;

        for (auto& v: hparg[u]) {
            if (scc[v]) continue;
            sfd(v);
        }
    };
	
    reverse(postorder.begin(), postorder.end());
    for (auto& u: postorder) {
        if (!scc[u]) ++sccCounter, sfd(u);
    }

    return scc;
}

Kosaraju 的特色

  • 比較好寫
  • 相對直觀
  • 不是 Tarjan
  • 常數比較大
  • 不是 Tarjan

\(2-\text{SAT}\)

  • 在資訊科學中,有個著名的問題,\(k\) 可滿足性問題
    • 布林變數(boolean variable):
      • \(v = 1 \leftrightarrow \neg v = 0\)
      • \(v = 0 \leftrightarrow \neg v = 1\)
    • 文字(literal):\(l = v\ /\ \neg v\)
    • 子句(clause):\(c = (l_1 \vee l_2 \vee l_3 \vee \cdots \vee l_k)\)
    • 合取範式(conjunctive normal form, CNF):
      • 符合形如 \(c_1 \land c_2 \land \cdots \land c_n\) 的布林算式
    • 析取範式(disjunctive normal form, DNF):
      • ​\(\vee\) 和 \(\land\) 互換的 CNF
  • ​​\(k-\text{SAT}\) 就是求解子句長度皆為 \(k\) 之 CNF 的問題

一開始

  • 我們先從 \(1-\text{SAT}\) 開始吧
  • 子句長度為 \(1\) 的 CNF 是什麼?
    • 形如 \(l_1 \land l_2 \land \cdots \land l_n\)
    • 例:\(\ a \land \neg b \land c \land \neg d\)
  • 不就是把所有東西都設成 true 就好嗎
    • 還有檢查有沒有 \(l_i \land \neg l_i\) 存在
    • 沒了,就這樣
  • 阿你是在期待什麼

回到 \(2-\text{SAT}\)

  • 所以這要怎麼解
    • 我都把它放在圖論了,應該跟圖論有點關係吧?
  • 我們先想一下在一個子句 \(c=(a \vee b)\) 中,有什麼性質吧
    • 在 \(c = 1\) 時,\(b = 1\) 是 \(\neg a = 1\) 的必要條件
    • 那 \(b = 1\) 是 \(\neg a = 1\) 的充分條件嗎?
      • ​不是!
    • 如果 \(c_1 \land c_2 \land \cdots \land c_n = 1\),則 \(c_i = 1\)
      • 若 \(c_i = (a_i,\,b_i)\):
        • \(\neg a \rightarrow b\)
        • \(\neg b \rightarrow a\)

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸
  • 然後呢?
  • 多一些狀態好了,例如 \((a \vee \neg b)\land(b \vee c)\land(\neg a \vee \neg c)\land(b \vee \neg d)\)
a
b
\neg a
\neg b

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸
  • 然後呢?
  • 多一些狀態好了,例如 \((a \vee \neg b)\land(b \vee c)\land(\neg a \vee \neg c)\land(c \vee \neg d)\)
a
b
\neg a
\neg b
c
d
\neg c
\neg d

什麼情況會無解

  • 當 \(a\) 和 \(\neg a\) 同時需要成立的時候

什麼情況會無解

  • 當 \(a\) 和 \(\neg a\) 同時需要成立的時候
  • \((a \vee \neg b)\land(b \vee \neg c)\land(\neg a \vee \neg b)\land(b \vee c)\)
a
\neg a
b
\neg b
c
\neg c

什麼情況會無解

  • 當 \(a\) 和 \(\neg a\) 同時需要成立的時候
  • \((a \vee \neg b)\land(b \vee \neg c)\land(\neg a \vee \neg b)\land(b \vee c)\)
    • ​當 \(a\) 和 \(\neg a\) 在同一個 SCC 中時!
a
\neg a
b
\neg b
c
\neg c

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解
  • \((a \vee \neg b)\land(b \vee \neg c)\land(a \vee b)\land(\neg b \vee \neg c)\)
a
\neg a
b
\neg b
c
\neg c

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解
  • \((a \vee \neg b)\land(b \vee \neg c)\land(a \vee b)\land(\neg b \vee \neg c)\)
  • 從沒有被需求的開始
    • 反過來拓樸排序!
a
\neg a
b
\neg b
c
\neg c

課後練習

  • 都會 1-SAT 和 2-SAT 了吧
    • 相信大家都會數學歸納法
  • 請回家解 3-SAT,寫完跟我對答案
  • 補充參考資料

例題

沒了

謝謝大家

建北電資 27th 演算法[18] 圖論[2]

By Brine

建北電資 27th 演算法[18] 圖論[2]

演算法[18] 連通性、連通分量

  • 369