Graph

By LFsWang

DFS Tree

DFS Tree

  • 由一個點做DFS之後構成的樹
  • 根據邊的在遍歷的過程中連接狀態有不同性質

DFS Tree

DFS Tree

定義\(R(v)\)表示點\(v\)在DFS Tree的父節點

而\(R(root)\)是空節點

Connected Component

連通分量

連通分量

設集合\(C\)為無向圖\(G\)的一個連通分量

若點\(x,y\in G\)

\(x,y \in C\Leftrightarrow x,y\)之間有一條路徑

連通分量

鄰點:若存在\(e(v,w)\),則v,w互為鄰點

因此\(v\)的所有鄰點\(N(v)\)是\(v\)的邊可以到達的所有點集合。

 

連通分量

路徑:若\(a,b\)在同一個連通塊,\(path(a,b)\)表示a到b的任意一種路徑。

 

連通分量

  1. 每一個點都位在一個連通分量
  2. 若有路徑\(path(x,y)\),就有\(path(y,x)\)
  3. 若存在\(path(x,y),path(y,z)\),就存在\(path(x,z)\)

看起來就像disjoint set

連通分量測試

定義函數\(C(g)\)表示圖\(g\)的連通分量數量,給定一張無向圖\(G\)

  • 求\(C(G)=?\)

DFS

根據連通分量的特性,DFS任意一個點會走遍該點的連通分量

 

因此就看用了幾次DFS就能知道答案

AP & Bridge

關節點與橋

Articulation Points

有一張無向連通圖\(G\)

若點\(v\)是關節點(AP)\(\Leftrightarrow\) \(G\)刪除\(v\)後,\(C(G)>1\)

Bridge

有一張無向連通圖

若邊\(e\)是橋\(\Leftrightarrow\) 從\(G\)刪除\(e\)後,\(C(G)>1\)

關節點與橋

給一張無向圖,請找出圖裡面所有的關節點 (與橋)。

 

DFS 爆搜

枚舉所有的點當關節點,模擬刪掉後,是否依然能遍歷所有的節點

橋亦同

 

DFS:\(O(E+V)\)

總複雜度:\(O((E+V)^2)\)

Tarjan's algo for AP

圖論大師Tarjan在1973年提出了在\(O(N)\)時間找出AP的演算法

實際上同方法也可以用來找Bridge

note.  O(N)在圖論表示O(E+V)

Tarjan's algo for AP

定義\(T\)是\(G\)的DFS搜索樹

對於點\(v \in G\),定義函數如下

 

\(D(v)\):表示點\(v\)在\(T\)的深度

$$D=1$$

$$D=2$$

$$D=3$$

$$D=4$$

Tarjan's algo for AP

定義\(T\)是\(G\)的DFS搜索樹

對於點\(v \in G\),定義函數如下

 

\(L(v)\):\(v\)、\(N(v)-R(v)\)

、\(v\)的子樹及子樹在G中所有

這團東西所有D(x)的最小值

Low point

自己

子樹

子樹臨點

L(x)就是打勾部分D(x)的最小值

臨點

Low point

a是v的臨點,不包含parent

b是v子樹的根結點

 

$$L(v) = min( D(v),D(a),L(b) )$$

Tarjan's algo for AP

定理1. Root r是AP\(\Leftrightarrow deg(r) > 1\)

 

Proof

若移除\(r\)會使原圖產生\(\deg(r)\)個連通分量

根據AP的定義得證

Tarjan's algo for AP

定理2. \(v\)是非root的點,w是\(v\)的鄰點。

若\(v\)是AP\(\Leftrightarrow \exists w :L(w)\geq D(v)\)

\(v\)

\(w\)

\(L(w)\)

因為w無法走到比v還要前面的點

 

移除v必然會至少分成w與r的兩個連通分量

\(r\)

Tarjan's algo for Bridge

定理3. \(v\)是非root的點,\(w\)是\(v\)的鄰點。

若\(e(v,w)\)是Bridge\(\Leftrightarrow \exists w :L(w) > D(v)\)

\(v\)

\(w\)

\(L(w)\)

因為w無法走到比w還要前面的點

 

移除e必然會至少分成w與r的兩個連通分量

\(r\)

\(e(v,w)\)

Tarjan's algo for AP

在Tarjan的原始論文中,D(x)是定義為DFS遍歷順序的時間戳,也是較為常見的實作方法

Tarjan's algo for AP

void DFS(int v, int fa) //call DFS(v,v) at first
{
    D[v] = L[v] = timestamp++; //timestamp > 0
    int childCount = 0; //定理1 for root
    bool isAP = false;

    for (int w:adj[v])
    {
        if( w==fa ) continue;
        if ( !D[w] ) // D[w] = 0 if not visited
        {
            DFS(w,v);
            childCount++;
            if (D[v]<=L[w]) isAP = true; //定理2
            if (D[v]< L[w]) edgeBridge.emplace_back(v, w);//定理3
            L[v] = min(L[v], L[w]);
        }
        L[v] = min(L[v], D[w]);
    }
    if ( v == fa && childCount < 2 ) isAP = false; //定理1, v==fa只是確認root
    if ( isAP ) nodeAP.push_back(v);
    return ;
}

練習題

給圖找Cutting Point

UVa 315

  • CP aka AP

Strongly Connected Component

強連通分量

SCC定義

令G是有向圖,\(v,w\)是G上的兩點,\(S\)是G的一個SCC,

$$v,w \in S \Leftrightarrow \exists path(v,w),path(w,v)$$

SCC由數個有向環構成!

SCC與縮點

縮點操作

將同一個強連通分量內的所有節點合併為一個點,構成一張新圖。

SCC與縮點

定理4.設\(G\prime\)是\(G\)經由縮點操作生成的圖

 

\(G\prime\)是有向無環圖(DAG)

SCC與縮點

Proof.

若\(G\prime\)有環,那這一個環構成一個SCC,根據縮點操作,這個環必須合併成一個點,故\(G\prime\)不存在環

SCC

有兩個線性方法可以求SCC

  1. Tarjan's SCC Algorithm
  2. Kosaraju's Algorithm

Tarjan's SCC Algorithm

圖論大師Tarjan在連通分量有許多著作,這也是其中一個

方法沿用了AP/Bridge的\(L(x),D(x)\)函數

Tarjan's SCC Algorithm

 定理5. 若點\(v\)有\(L(v)=D(v)\),\(v\)子樹所有未縮點的點構成SCC

Tarjan's SCC Algorithm

void DFS(int v, int fa) { //call DFS(v,v) at first
    D[v] = L[v] = timestamp++; //timestamp > 0
    st.push(v);

    for (int w:adj[v]) {
        if( w==fa ) continue;
        if ( !D[w] ) { // D[w] = 0 if not visited
            DFS(w,v);
            L[v] = min(L[v], L[w]);
        }
        L[v] = min(L[v], D[w]);
    }
    if( D[v] == L[v] ) {
        int x;
        do{
            x = st.top();
            st.pop();
            scc[x] = SCCID;
        }while( x != v );
        SCCID++;
    }
}

Kosaraju's Algorithm

兩次DFS的方法

 

用後序走訪走過整張圖放入stack

由stack一個一個拿出來在反圖DFS,走到的都是該點的SCC

Kosaraju's Algorithm

個人獨門方法 比某模板短一半的code

void DFS(vector<int> *dG, int v, int k=-1){
    visited[v] = true;
    contract[v] = k;
    for (int w:dG[v])
        if (!visited[w])
            DFS(dG,w,k);
    if(dG==G) st.push(v);
}

int Kosaraju(int N){
    memset(visited,0,sizeof(visited));
    for(int i=0; i<N; ++i) if(!visited[i]) DFS(G,i);
    memset(visited,0,sizeof(visited));
    while(!st.empty()){
        if (!visited[st.top()]) DFS(GT, st.top(), sccID++);
        st.pop();
    }
    return sccID;
}

練習題

給定一些骨牌,且已知推倒每張骨牌會連動哪一些骨牌,問至少需推倒幾張骨牌才能使所有骨牌都倒下。

UVa 11504

2-SAT

滿足性問題

SAT問題

以AND運算將許多OR運算式合併成的算式

 

$$(a \lor b \lor c )\land (c \lor \neg b \lor d )\land (e)=1?$$

 

如果在最多OR的子運算式中,最多只有K個變數,那麼我們稱這一個問題為K-SAT問題。

SAT問題

定理7. 所有SAT問題都能轉換成3-SAT問題

定理6. 所有NPC問題都能轉換成SAT問題

定理8. 3-SAT問題是NPC問題

2-SAT

如果3-SAT問題可以被進一步化簡為2-SAT問題

定理9. 2-SAT問題是P問題

Krom's Algorithm

可以在\(O(N)\)的時間判定2-SAT問題

$$(a \lor b)\land (c \lor \neg b )\land (e)=1?$$

 

運用SSC的概念

依賴關係

考慮布林式\(a \lor b\),若要讓此式子為true

 

若a選擇false,b一定要選true

若b選擇false,a一定要選true

依賴關係

將2SAT依賴關係表示成一張圖\(G_r\)

有向邊(A,B)表示若選擇A,就要選擇B

$$B\prime$$

$$A\prime$$

$$A$$

$$B$$

\(X\prime\)表示X使用false的狀態

依賴關係

怎麼連 ?

( not a or b )

( a )

( not a )

( a xor b )

Krom's Algorithm

定理10. 若2SAT問題無解,則存在X使

\(X,X\prime\)位在同一個SCC

反之,可以構造一組可行的解

X = true

X = false

Krom's Algorithm

一種構造方法

var[i] = scc[i]>scc[not(i)];
//Kosaraju作的DAG,SCC深度深的ID比較大

沿著縮點完的DAG回朔依賴關係即可獲的解答

練習題

TNFSH ZJ B017

Lowest Common Ancestor

最近共同祖先

定根

給一個隨便的無根樹很麻煩,所以我們隨便挑一個點當作根

LCA

定義:點(x,y)往root的最短路徑上,第一個共有的點

LCA(x,y)

x

y

樹上兩點間詢問

如果詢問的答案可以透過扣除來調整,可以考慮LCA

 

ex 兩點間最短路徑長

$$L(x,y) = L(R,x)+L(R,y)-2\times L(R,lca(x,y))$$

倍增法

求一個點的\(2^k\)祖先是哪一點,使用DP來完成

定義dp[i][j]表示點i的第\(2^j\)個祖先節點是誰

 

$$dp[i][j]=\begin{cases}-1 & \text{not exist} \\ fa(i)&j=0\\dp[dp[i][j-1]][j-1]&dp[i][j-1]\neq -1\end{cases}$$

LCA DP

for (int i=0; i<lgN; ++i)
    for (int x=0; x<n; ++x)
    {
        if (P[x][i]==-1) P[x][i+1] = -1;
        else P[x][i+1] = P[P[x][i]][i];
    }

先用dfs求dp[i][0]的答案

Jump

使用DP表格,可以在\(O(logN)\)的時間求出\(v\)的任意遠祖先

把移動距離二進位分解

再移動即可

int jump(int x,int d)
{
    for(int i=0;i<lgN;++i)
        if( (d>>i)&1 )
            x=dp[x][i];
    return x;
}

LCA

使用DP表格+Jump求LCA

int find_lca(int a,int b){
    if(dep[a]>dep[b])
        swap(a,b);
    b=jump(b,dep[b]-dep[a]);
    if(a==b)
        return a;
}

Step 1. 調整深度

 

把a,b移動到相同深度的位置去

 

如果移動完a=b就完成

LCA

使用DP表格+Jump求LCA

int find_lca(int a,int b){
    if(dep[a]>dep[b])
        swap(a,b);
    b=jump(b,dep[b]-dep[a]);
    if(a==b)
        return a;
    for(int i=MAX_LOG;i>=0;--i)
    {
        if(dp[a][i]!=dp[b][i])
        {
            a=dp[a][i];
            b=dp[b][i];
        }
    }
 
}

Step 2. 二分搜

 

k由大到小跳\(2^k\)步

如果是同樣的點,就不動,不然就跳上去

LCA

使用DP表格+Jump求LCA

int find_lca(int a,int b){
    if(dep[a]>dep[b])
        swap(a,b);
    b=jump(b,dep[b]-dep[a]);
    if(a==b)
        return a;
    for(int i=MAX_LOG;i>=0;--i)
    {
        if(dp[a][i]!=dp[b][i])
        {
            a=dp[a][i];
            b=dp[b][i];
        }
    }
    return dp[a][0];
}

Step 3.完成

 

最後會停再LCA下方

丟回dp答案即可

樹上詢問

你得到了一棵N個節點的樹!!(頂點編號1~N)

你選定了一個起點S跟一個終點T,

現在你從S走到T,請求出走第K步時走到的點。

(K=0表示還待在S)。

TIOJ 1687

樹序列化

樹序列化

  • 又稱作樹壓平
  • 在進階資料結構中,有提到很多處理一維序列的方法
  • 如果不知道Tree的問題怎麼解,是否有辦法把樹轉成一圍序列?

歐拉走訪

  • 沿著樹的邊緣走一圈
  • DFS進去與出來都看一次

歐拉走訪

  • 走訪結果

\(\{1,2,5,2,6,2,7,2,1,3,1,4,1\}\)

  • 重要性質

一個點進去與出來構成的區間,包含子樹的所有點

Lowest Common Ancestor

最近共同祖先

 更神奇的作法

  • 可以做到\(O(n)\)預處裡,\(O(1)\)查詢

Method of Four Russians

4個俄羅斯人法(!?),把\(O(\log n)\)壓到\(O(1)\)的技巧

LCA與樹序列化

1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0

LCA與樹序列化

1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0

A與B的LCA是:

第一個A與最後一個B構成的區間中,深度最小的點

區間最小值

1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0

觀察到剛剛的特性後,就能把LCA問題變成RMQ問題

不過有學過\(O(n)\)預處裡\(O(1)\)查詢的資料結構嗎?

Sparse Table

  • 可以在\(O(n\log n)\)預處理,\(O(1)\)查詢的資料結構

st[x][y] = \(a_x\)到\(a_{x+2^y-1}\)的答案(共\(2^y\)個元素)

查詢任意範圍a~b,只要查詢

$$min\begin{cases} st[a][c] \\ st[b-2^c][c]\end{cases} ,c=\lfloor\log_2(b-a+1)\rfloor$$

c=std::__lg(b-a+1);

騙我!

  • 這方法沒有\(O(n)\)預處裡阿!
  • 別急,還有其他方法

建表法

  • 因為數列相鄰的數字只有\(+1,-1\)兩種可能
  • 可以考慮數列的差分序列
1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0
+1 +1 -1 +1 -1 +1 -1 -1

建表法

  • 對於一個差分序列還原出來的原始數列,最小值的位置是固定的!結果與原數列無關

若\(a,b,c,d,e\)的差分序列是

\(<1,-1,-1,-1,>\)

最小值一定是\(e\)

建表法

  • 預先把所有可能的差分序列最小值的位置建表
  • 因為差分序列只有兩種數字,可以用二進位表示

\(O(n2^n)預處裡\)

\(O(1)查詢\)

看起來超級爛?

分塊大法

  • 如果把數列每\(K\)個資料切一塊,預先算出每一塊的最小值

\(O(\frac{N}{K}+K)\)找出最小值

\(O(N)\)預處裡

1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0
0 1 1 1 0

分塊大法+Sparse Table

  • 把大塊的資料做Sparse Table,就能在\(O(1)\)時間算大塊最小值

\(O(1+K)\)找出最小值

\(O(\frac{K}{N}\log \frac{K}{N} +N)\)預處裡

0 1 1 1 0

分塊大法+Sparse Table+建表法

  • 把小塊的資料做建表法,就能在\(O(1)\)時間算小塊最小值

\(O(1+1)\)找出最小值

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)預處裡

1 2 5 2 6 2 7 2 1
深度 0 1 2 1 2 1 2 1 0

數學的時間

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)

K要怎麼設才能讓複雜度變好?

數學的時間

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)

\(K=c\log N\)

數學的時間

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)

\(K=c\log N\)

\(\frac{K}{N}\log \frac{K}{N}\\=\frac{c\log N}{N}\log \frac{c\log N}{N}\\=\frac{c\log N}{N}(\log{c\log N}-\log{N})=o(N)\)

\(O\)表示小於等於的話,\(o\)就表示小於

數學的時間

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)

\(K=c\log N\)

\(K2^K\\=c\log{N}2^{c\log{N}}=c\log{N}2^{\log{N^c}}\\=c\log{N}\times N^c\)

數學的時間

\(O(\frac{K}{N}\log \frac{K}{N} + K2^K+ N)\)

\(K=c\log N\)

\(c\log{N}\times N^c\)

設\(c=0.5\)

\(0.5\log{N}\sqrt{N}\leq 0.5\sqrt{N}\sqrt{N}\in O(N)\)

LCS

分塊大法+Sparse Table+建表法

  • 把K設為\(0.5\log{N}\)

\(O(1)\)找出最小值

\(O(N)\)預處裡

樹鏈剖分

樹鏈剖分

  • 利用尤拉路徑做序列化,能解決子樹有關的問題,但路徑問題就比較麻煩了
  • 樹練剖分是另一種把樹轉換成序列操作的方法

樹序列化-樹練剖分

把樹分解成許多線段,查詢時把幾個線段上的答案合併

要怎麼切線段?

Robert Tarjan's

Heavy Light Decomposition

  • 將非葉節點的點,選擇重邊連結
  • 其餘的邊為輕邊,會被分解掉

輕重邊

  • 重邊:最多子節點的子樹
  • 輕邊:不是重邊的邊

參閱掛長的動畫

Graph

By sylveon

Graph

  • 1,430