Graph[2]

algorithm[18] 22527 Brine

我是誰

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

連通性

連通性

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

連通性

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

連通度的重要性

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

連通度的重要性

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

連通度的重要性

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

連通度的重要性

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

連通度的重要性

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

連通度的重要性

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

割點 Cut Vertex

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

割點 Cut Vertex

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

割點 Cut Vertex

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

割點 Cut Vertex

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

暴力作法

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

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

如何判斷誰是割點

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

如何判斷誰是割點

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

如何判斷誰是割點

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

DFS 樹的根節點 rr

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

利用割點性質

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

一位傳奇人物

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

Tarjan 的想法

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

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

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

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

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

Bridge

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

Bridge

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

Bridge

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

Bridge

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

還是 Tarjan 的想法

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

還是 Tarjan 的想法

還是 Tarjan 的想法

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

連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

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

雙連通分量

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

把圖「縮起來」

  • 邊雙連通分量

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
66
6
55
5
00
0
12391239
1239
487487
487

把圖「縮起來」

  • 點雙連通分量

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
44
4
99
9
00
0

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
44
4
99
9
00
0

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 點雙連通分量
  • 看起來沒有縮欸
22
2
55
5
44
4
99
9
12391239
1239
00
0
66
6
487487
487

縮完點之後可以幹嘛

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

邊雙連通分量

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

點雙連通分量

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

點雙連通分量

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

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
22
2
44
4
12341234
1234
00
0
55
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,vu, v,存在 uvu \rightarrow v路徑
      • 注意不用「拔掉一個點或邊仍然成立」,不用雙連通
    • 弱連通分量(weakly connected component)
      • 對於任意兩點 u,vu, v,存在 uvu \rightarrow v vuv \rightarrow u 的路徑
      • 注意弱連通分量的定義和弱連通圖不太一樣
        • ​為什麼?
        • 我的解釋:「這種定義應用比較多」

求強連通分量的意義

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

求強連通分量的意義

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

求強連通分量的意義

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

求強連通分量的意義

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

求強連通分量的意義

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

Tarjan's SCC

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

樹邊 vs. 前向邊

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

返祖邊 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)G(V, E),若有一個圖:
        • 和它有相同的點集
        • 所有的邊 (u,v)(u, v) 都有能一一對應的邊 (v,u)(v, u)
      • 則稱這張圖為 GG 的反圖,記為 GG'GT,GRG^T, G^R \cdots
    • 反圖可以拿來幹嘛?

反圖的性質

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

反圖的性質

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

反圖的性質

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

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧
    • 反圖和原圖具有相同的 SCC!
    • 若在原圖中,存在一條 uuvv 的路徑
      • 反圖也存在一條 uuvv 的路徑 \leftrightarrow u,vu, 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

2SAT2-\text{SAT}

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

一開始

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

回到 2SAT2-\text{SAT}

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

把狀態化成圖

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

把狀態化成圖

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

把狀態化成圖

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

什麼情況會無解

  • aa¬a\neg a 同時需要成立的時候

什麼情況會無解

  • aa¬a\neg a 同時需要成立的時候
  • (a¬b)(b¬c)(¬a¬b)(bc)(a \vee \neg b)\land(b \vee \neg c)\land(\neg a \vee \neg b)\land(b \vee c)
aa
a
¬a\neg a
\neg a
bb
b
¬b\neg b
\neg b
cc
c
¬c\neg c
\neg c

什麼情況會無解

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

如果不是強連通?

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

如果不是強連通?

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

如果不是強連通?

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

課後練習

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

例題

沒了

謝謝大家

Made with Slides.com