圖論

圖論

圖論是數學的一個分支。它以圖為研究對象。圖論中的圖是由若干給定的點及連接兩點的線所構成的圖形,這種圖形通常用來描述某些事物之間的某種特定關係,用點代表事物,用連接兩點的線表示相應兩個事物間具有這種關係。

歐拉迴路

前置知識

dfs(深度優先搜索)

深度優先搜索是一種在開發爬蟲早期使用較多的方法。它的目的是要達到被搜索結構的葉結點(即那些不包含任何超鏈的HTML文件) 。在一個HTML文件中,當一個超鏈被選擇後,被鏈接的HTML文件將執行深度優先搜索,即在搜索其餘的超鏈結果之前必須先完整地搜索單獨的一條鏈。深度優先搜索沿著HTML文件上的超鏈走到不能再深入為止,然後返回到某一個HTML文件,再繼續選擇該HTML文件中的其他超鏈。當不再有其他超鏈可選擇時,說明搜索已經結束。

邊權

離散數學或數據結構中,圖的每條邊上帶的一個數值,它代表的含義可以是長度等等,這個值就是邊權

歐拉路徑

如果圖中的一個路徑包括每個邊恰好一次,則該路徑稱為歐拉路徑。

歐拉迴路

首尾相接的歐拉路徑稱為歐拉迴路。

判定

由於每一條邊都要經過恰好一次,因此對於除了起點和終點之外的任意一個節點,只要進來,一定要出去。

一個無向圖存在歐拉迴路,當且僅當該圖所有頂點度數都為偶數,且該圖只有一個存在邊的連通塊。

一個無向圖存在歐拉路徑,當且僅當該圖中奇點的數量為0或2,且該圖只有一個存在邊的連通塊。

一個有向圖存在歐拉迴路,當且僅當所有點的入度等於出度。

一個混合圖存在歐拉迴路,當且僅當存在一個對所有無向邊定向的方案,使得所有點的入度等於出度。需要用網路流。

求法

我們用 dfs來求出一張圖的歐拉迴路。

我們給每一條邊一個 vis數組代表是否訪問過,接下來從一個點出發,遍歷所有的邊。

直接dfs並且記錄的話會有一些問題。

為了解決這個問題,我們在記錄答案的時候倒著記錄,也就是當我們通過 (u, v) 這條邊到達 v 的時候,先把 v dfs 完再加入 (v, u) 這條邊。

還有一點需要注意。因為一個點可能被訪問多次,一不小心可能會寫成 O(n 2 ) 的(因為每次遍歷所有的出邊)。解決方案就是設一個cur數組,每次直接從上一次訪問到的出邊繼續遍歷。

時間複雜度 O(n + m)。

程式碼

void dfs(int x)
{
    for(int&hd=head[x];hd;hd=e[hd].nxt)
    {
        if(flag[hd>>1])continue;
        flag[hd>>1]=1;
        dfs(e[hd].to);
        a[++top]=x;
    }
}

拓撲排序

定義

所謂拓撲排序,就是把有向圖上的 n 個點重新標號為 1 到 n,滿足對於任意一條邊 (u, v),都有 u < v。

並不是所有的圖都能進行拓撲排序,只要圖中有環,那麼就可以導出矛盾。

可以進行拓撲排序的圖稱為有向無環圖(DAG),有很多優美的性質,比如可以在拓撲序上進行 DP(動態規劃)。

我們記錄一下每一個點的入度和出度,用一個隊列維護當前所有入度為 0 的點。 每次拿出來一個入度為 0 的點並且將它加到拓撲序中,然後枚舉出邊更新度數。 時間複雜度O(n + m)。

(在拓撲排序的過程中可以順帶進行 DP)

程式碼

    for(int i=1;i<=n;i++)
        if(d[i]==0)q.push(i);
    while(!q.empty())
    {
        int node=q.front();
        q.pop();
        res[++top]=node;
        for(int hd=head[node];hd;hd=e[hd].nxt)
        {
            d[e[hd].to]--;
            if(d[e[hd].to]==0)
                q.push(e[hd].to);
        }
    }

最短路

所謂最短路,就是把邊權看做邊的長度,從某個點 S到另一個點 T 的最短路徑。

用更加數學化的語言描述就是,對於映射 f : V → R,滿足 f(S) = 0 且 ∀(x, y, l) ∈ E, |f(x) − f(y)| ≤ l 的情況下,f(T) 的最大值。

單源最短路——Dijkstra

在所有的邊權均為正的情況下,我們可以使用 Dijkstra 算 法求出一個點到所有其它點的最短路徑。

我們維護一個集合,表示這個集合內的點最短路徑已經確定了。

每次我們從剩下的點中選擇當前距離最小的點 u 加入這個集合,然後枚舉另一個點 v 進行更新:

dv = min(dv, du + w(u, v))

直接這樣做時間複雜度是 O(n 2 ) 的。

優化

我們注意到,複雜度主要來源於兩個地方。

第一個是找出當前距離最小的點。這個可以用堆很容易地實現。

第二個是枚舉 v,如果我們用鄰接表存圖,可以降到邊數級別。

這樣我們就把複雜度降到了 O((n + m) log n)。

單源最短路——Bellman-Ford

另一種求單源最短路的演算法,複雜度不如Dijkstra優秀。

考慮在上面出現過的鬆弛操作:

dv = min(dv, du + w(u, v))

由於最短路徑只會經過最多 n 個點,因此每一個點的最短 路徑只會被鬆弛至多 n − 1 次。

所以我們可以對整張圖進行 n − 1 次鬆弛操作,每次枚舉所有的邊進行更新。

時間複雜度 O(nm)。

SPFA

它死了。(不要用,它的複雜度是錯誤的)

應用:費用流

Bellman-Ford 演算法不夠優秀,於是我們嘗試改進這個演算法。

注意到,在進行鬆弛操作的時候,如果點 u 的距離一直沒有發生變化,那麼就不需要再枚舉這個點的出邊進行鬆弛了。

也就是說我們可以用一個隊列保存所有距離發生變化的點, 每次取出一個點進行更新。

於是 SPFA就誕生了。

如果圖是隨機的,SPFA的期望時間複雜度約為 O(2m),比 之前提到的任何一個演算法都優秀,而且還可以有負權。

但是在最壞情況下它的複雜度和 Bellman-Ford 相同,都是 O(nm),在正式比賽中,沒有哪個出題人會放過它。(因為其複雜度本來就是錯的)

多源最短路——Floyd

對於一張圖,我們希望求出任意兩個點之間的最短路徑。

我們用 DP(動態規劃) 的思想。設 fi,j,k 表示從 i 到 j,途中僅經過前 k個點的最短路。

由於每一個點在最短路中只會出現一次(不然就出現負環了,不存在最短路),所以可以很寫出轉移方程:

fi,j,k = min(fi,j,k−1, fi,k,k−1 + fk,j,k−1)

時間複雜度是 O(n 3 )。

在實際求的過程中,最後一維可以用滾動數組優化掉,所以空間複雜度是O(n 2 )。

程式碼

for(int k=1;k<=n;k++)

  for(int i=1;i<=n;i++)

    for(int j=1;j<=n;j++)

    dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);

注意三層循環的順序不能顛倒。

Floyd 傳遞閉包

有時候,我們需要維護一些有傳遞性的關係,比如相等,連通等等。(12連通,23連通,則 13連通)

初始條件往往是已知若干個點對具有這些關係,然後讓你弄 出來所有的關係。

可以直接把 Floyd 演算法做一下調整——

dis[i][j]=dis[i][j]|(dis[i][k]&dis[k][j]);

這個演算法叫做傳遞閉包。

多源最短路——Johnson 重賦權

對於多源最短路,如果我們枚舉一個點然後跑堆優化的 Dijkstra,那麼複雜度是 O(nm log n) 的,在圖比較稀疏的情況下,這個複雜度要優於 Floyd 演算法的 O(n 3 )。

但是 Dijkstra 演算法要求所有邊權均非負。

於是就有了重賦權的技巧。

我們新建一個 0 號點,並且從這個點出發向所有點連一條邊 權為 0 的邊,然後跑單源最短路。(SPFA 或者 Bellman-Ford)

設距離數組為 h,接下來對於每條邊 (u, v),令 w ′ (u, v) = w(u, v) + h(u) − h(v)。

這樣所有的邊權就都變成非負了,我們就可以跑 Dijkstra 演算法了。

證明

首先由於 h(v) ≤ h(u) + w(u, v),新圖的邊權一定非負。

設新圖上的最短路徑為 d ′,原圖上的最短路徑為 d。

d ′ (u, v) = min a1,a2,…,ak w ′ (u, a1) + w ′ (a1, a2) + · · · + w ′ (ak, v)

= min a1,a2,…,ak w(u, a1) + (h(u) − h(a1)) + w(a1, a2)+ (h(a2) − h(a1)) + · · · + w(ak, v) + (h(v) − h(ak))

= h(u) − h(v) + min a1,a2,…,ak w(u, a1) + · · · + w(ak, v)

= h(u) − h(v) + d(u, v)

最短路樹(最短路圖)

所謂最短路樹,就是在求完從 S 出發的單源最短路之後,

只保留最短路上的邊形成的數據結構。

只需要在求的過程中維護一個pre數組表示這個點的前驅即可。很多最短路的變種都需要用這個演算法。

最小生成樹

Prim 演算法

類比 Dijkstra 演算法,我們維護一個集合 S,表示這個集合中 的生成樹已經確定了。

演算法流程和Dijkstra一樣,唯一的區別是用w(u, v) 去更新 dv 而不是用 du + w(u, v)。

時間複雜度 O(n 2 ),同樣可以用堆優化。

Kruskal 演算法

前置知識

並查集演算法

並查集主要用於解決一些元素分組的問題。它管理一系列不相交的集合,並支援兩種操作:

合併:把兩個不相交的集合合併為一個集合。

查詢:查詢兩個元素是否在同一個集合中。

優化

路徑壓縮(O(logn))+安值合併(O(logn))→O(αn)(αn在108數據內不超過4,可視為常數)

程式碼

find(int x)
{
    return x==pa[x]?x:pa[x]=find(pa[x])
}

因為是求的最小生成樹,所以我們用貪心的思路,把所有的邊權從小到大排序,然後一條一條嘗試加入,用並查集維護連通性。

可以發現這樣一定能得到原圖的最小生成樹。

證明

如果某一條邊 (u, v) 不屬於最小生成樹,那麼考慮最小生成樹上 連接 u, v 的路徑,這上面一定有一條邊權不小於 w(u, v) 的邊 (因為我們是從小到大枚舉的所有邊),這樣替換後答案一定不會變劣。

時間複雜度 O(m log m)。

Kruskal 重構樹

前置知識

dfs(深度優先搜索)

LCA(最近公共祖先)

在一棵沒有環的樹上,每個節點肯定有其父親節點和祖先節點,而最近公共祖先,就是兩個節點在這棵樹上深度最大的公共的祖先節點。

LCA主要是用來處理當兩個點僅有唯一一條確定的最短路徑時的路徑。

樹上倍增

用於求LCA(最近公共祖先)。

倍增的思想是二進位。

首先開一個n×logn的數組,比如fa[n][logn],其中fa[i][j]表示i節點的第2^j個父親是誰。

然後,我們會發現一個性質:

fa[i][j]=fa[fa[i][j-1]][j-1]

用文字敘述為:i的第2^j個父親 是i的第2^(j-1)個父親的第2^(j-1)個父親。

這樣,本來我們求i的第k個父親的複雜度是O(k),現在複雜度變成了O(logk)。

 

Kruskal 重構樹是基於 Kruskal 最小生成樹演算法的一種算 法,它主要通過將邊權轉化為點權來實現。

流程

將所有邊按照邊權排序,設 r(x) 表示 x 所在連通塊的根節點。(注意這裡要用並查集)

枚舉所有的邊 (u, v),若 u, v 不連通,則新建一個點 x,令 x 的權值為 w(u, v)。 連接 (x, r(u)) 和 (x, r(v))。 令 r(u) = r(v) = x。

不斷重複以上過程,直到所有點均連通。

 

時間複雜度 O(m log m)。

性質

這樣,我們就得到了一棵有 2n − 1 個節點的二叉樹,其中葉節點為原圖中的點,其餘的點代表原圖中的邊,並且滿足父節點權值大於等於子節點。

它有什麼用呢?

求 u, v 之間路徑上的最大邊權 → 求重構樹上 u, v 兩個點的 LCA。

只保留邊權小於等於 x 的邊形成的樹 → 重構樹上點權小於 等於 x 的點的子樹。

 

Borůvka 演算法

前置知識

距離

(x1,y1)(x2,y2

曼哈頓距離:|x1-x2|+|y1-y2|

切比雪夫距離:max(|x1-x2|,|y1-y2|

歐幾里得距離:√[(x1-x2)2+(y1-y2)2]

曼哈頓距離與切比雪夫距離的相互轉化

兩者之間的關係

我們考慮最簡單的情況,在一個二維坐標系中,設原點為(0,0)。

如果用曼哈頓距離表示,則與原點距離為1的點會構成一個邊長為2–√2的正方形。

 

如果用切比雪夫距離表示,則與原點距離為1的點會構成一個邊長為2的正方形。

 

對比這兩個圖形,我們會發現這兩個圖形長得差不多,他們應該可以通過某種變換互相轉化。

事實上,

將一個點(x,y)的坐標變為(x+y,x−y)後,原坐標系中的曼哈頓距離 =新坐標系中的切比雪夫距離。

將一個點(x,y)的坐標變為((x+y)/2,(x−y)/2) 後,原坐標系中的切比雪夫距離 = 新坐標系中的曼哈頓距離。

(注意:切比雪夫距離轉曼哈頓距離要再除以二)

 用處

切比雪夫距離在計算的時候需要取max,往往不是很好優化,對於一個點,計算其他點到該的距離的複雜度為O(n)。

而曼哈頓距離只有求和以及取絕對值兩種運算,我們把坐標排序後可以去掉絕對值的影響,進而用前綴和優化,可以把複雜度降為O(1)。

 

第三種求最小生成樹的演算法,雖然比較冷門但是很多題需要用到這個演算法。

我們維護當前形成的所有連通塊,接下來對於每一個連通塊,找到邊權最小的出邊,然後合併兩個連通塊。

不斷重複這個操作,直到整張圖變成一個連通塊。

由於每次操作連通塊數量至少減半,所以時間複雜度最壞為 O((n + m) log n),隨機圖的話複雜度可以降到 O(n + m)。

Tarjan演算法

Tarjan 演算法不是某個特定的演算法,而是一群演算法。

強連通分量

割點/割邊/橋

點雙連通分量

邊雙連通分量

離線 O(n) 求 LCA

此外還有很多 Tarjan 獨立/合作創造的演算法:

Splay,LCT, 斐波那契堆,斜堆,配對堆,可持久化數據結構,……

有向圖——強連通分量

如果對於兩個點 u, v,同時存在從 u 到 v 的一條路徑和從 v 到 u 的一條路徑,那麼就稱這兩個點強連通。

如果一張圖的任意兩個點均強連通,那麼就稱這張圖為強連通圖。

強連通分量指的是一張有向圖的極大強連通子圖。 (極大≠最大)

Tarjan 演算法可以用來找出一張有向圖的所有強連通分量。

我們用 dfs的方式來找出一張圖的強連通分量。

建出 dfs 樹,記錄一下每一個節點的時間戳(dfn),然後我們考慮強連通分量應該滿足什麼條件。

我們可以再記錄一個 low 數組,表示每一個點能夠到達的最小的時間戳,如果一個點的 dfn=low,那麼這個點下方就形成了一個強連通分量。

在 dfs 的過程中,對於 (u, v) 這條邊:

若 v 未被訪問,則遞歸進去 dfs 並且用 low[v] 更新 low[u]。

若 v 已經被訪問並且在棧中,則直接用 dfn[v] 更新 low[u]。

最後如果 dfn[u]=low[u],則直接把棧中一直到 u 的所有點 拿出來作為一個強連通分量。

時間複雜度 O(n)。

有向圖——縮點

跑出來強連通分量之後,我們可以把一個強連通分量看成一 個點。

接下來枚舉所有的邊,如果是一個強連通分量里的就忽略, 否則連接兩個對應的強連通分量。這個操作稱為縮點。

縮點後就變成了一張有向無環圖,處理連通性問題的時候會方便很多。

無向圖——割點

對於一張無向圖,我們希望求出它的割點。

無向圖的割點定義為刪掉這個點之後,連通塊數量會發生改變的點。

類比上面,我們還是記錄一下 dfn(時間戳)和 low。

對於 u 的一個子節點 v,若 dfn[u]≤low[v],則 u 是割點(因 為 v 無法繞過 u 往上走)。

不過需要注意兩點:

根節點不能用這種方法,而是應該看它的子節點數量是否大於等於 2,如果是那麼根節點就是割點。

枚舉出邊的時候要特判掉父子邊的情況。

無向圖——橋

無向圖的橋定義為刪掉這條邊後,連通塊數量會發生改變的邊。

和上面的方法幾乎一模一樣,唯一的區別是判斷dfn[u]<low[v]而不是dfn[u]≤low[v]。(如果從 v 出發連 u 都無法 到達,那麼 (u, v) 就是一個橋邊)

甚至連根節點都不需要特判了。

無向圖——點/邊雙連通分量

如果兩個點之間存在兩條點互不相交的路徑,那麼就稱這兩個點是點雙連通的。

如果兩個點之間存在兩條邊互不相交的路徑,那麼就稱這兩個點是邊雙連通的。

其餘的定義參考強連通分量。                       

割點將整張圖分成了若干個點雙連通分量,並且一個割點可以在多個點雙連通分量中。

而橋則把整張圖拆成了若干個邊雙連通分量,並且橋不在任意一個邊雙連通分量中。

魔改一下強連通分量演算法即可。

當然,無向圖也可以縮點,不過主要還是可以用來建圓方樹。

二分圖匹配

前置知識

匹配

在圖論中,一個匹配是一個邊的集合, 其中任意兩條邊都沒有公共頂點。

最大匹配

一個圖所有匹配中,所含匹配邊數最多的匹配, 稱為這個圖的最大匹配。

如果要求一般圖的最大匹配,需要用 O(n 3 ) 的帶花樹,至少 是 NOI+ 的演算法。在聯賽階段,我們一般只關注二分圖的匹配問題。

最大匹配——匈牙利演算法

完美匹配

如果一個圖的某個匹配中,所有的頂點都是匹配點,那麼它就是一個完美匹配。

二分圖

如果一個圖的頂點能夠被分為兩個集合 X, Y,滿 足每一個集合內部都沒有邊相連,那麼這張圖被稱作是一張二分圖。

(dfs可以判斷一張圖是否是二分圖)

交替路

從一個未匹配點出發,依次經過非匹配邊——匹配 邊——非匹配邊——……形成的路徑叫交替路。

增廣路

從一個未匹配點出發,依次經過非匹配邊——匹配 邊——非匹配邊——……——非匹配邊,最後到達一個未匹配點形成的路徑叫增廣路。

注意到,一旦我們找出了一條增廣路,將這條路徑上所有匹配邊和非匹配邊取反,就可以讓匹配數量+1。

匈牙利演算法就是基於這個原理。

假設我們已經得到了一個匹配,希望找到一個更大的匹配。

我們從一個未匹配點出發進行 dfs(深度優先搜索),如果找出了一個增廣路, 就代表增廣成功,我們找到了一個更大的匹配。

如果增廣失敗,可以證明此時就是最大匹配。

由於每個點只會被增廣一次,所以時間複雜度是 O(n(n + m))。

二分圖最大權匹配——KM 演算法

現在我們把所有的邊都帶上權值,希望求出所有最大匹配中權值之和最大的匹配。

我們的思路是給每一個點賦一個「期望值」,也叫作頂標函數 c,對於 (u, v) 這條邊來說,只有 c(u) + c(v) = w(u, v) 的時 候,才能被使用。

容易發現,此時的答案就是 ∑c(i)。

初始,我們令左邊所有點的 c(u) = maxv w(u, v),也就是說最理想的情況下,每一個點都被權值最大的出邊匹配。

接下來開始增廣,每次只找符合要求的邊。我們定義只走這些邊訪問到的子圖為相等子圖。

如果能夠找到增廣路就直接增廣,否則,就把這次增廣訪問到的左邊的所有點的 c − 1,右邊所有點的 c + 1。

經過這樣一通操作,我們發現原來的匹配每一條邊仍然滿足條件。同時由於訪問到的點左邊比右邊多一個(其餘的都匹配上了),所以這樣會導致總的權值−1。

接下來再嘗試進行增廣,重複上述過程。直接這樣做時間復 雜度是 O(n 3 c) 的。(進行 n 次增廣,每次修改 c 次頂標,訪問所有 n 2 條邊)

優化

由於修改頂標的目標是讓相等子圖變大,因此可以每次加減 一個最小差值 delta。這樣每次增廣只會被修改最多 n 次頂標,時間複雜度降到 O(n 4 )。

注意到每次重新進行 dfs(深度優先搜索) 太不優秀了,可以直接進行 bfs, 每次修改完頂標之後接著上一次做。時間複雜度降到 O(n 3 )。

技巧

最小點覆蓋

選取最少的點,使得每一條邊的兩端至少有一 個點被選中。

二分圖的最小點覆蓋 = 最大匹配

證明

1.由於最大匹配中的邊必須被覆蓋,因此匹配中的每一個點對 中都至少有一個被選中。

2.選中這些點後,如果還有邊沒有被覆蓋,則找到一條增廣路,矛盾。

最大獨立集:選取最多的點,使得任意兩個點不相鄰。

最大獨立集 = 點數-最小點覆蓋

證明

1.由於最小點覆蓋覆蓋了所有邊,因此選取剩餘的點一定是一個合法的獨立集。

2.若存在更大的獨立集,則取補集後得到了一個更小的點覆蓋,矛盾。

最小邊覆蓋:選取最少的邊,使得每一個點都被覆蓋。

最小邊覆蓋 = 點數-最大匹配

證明

1.先選取所有的匹配邊,然後對剩下的每一個點都選擇一條和 它相連的邊,可以得到一個邊覆蓋。

2.若存在更小的邊覆蓋,則因為連通塊數量 = 點數-邊數,這 個邊覆蓋在原圖上形成了更多的連通塊,每一個連通塊內選一條邊,我們就得到了一個更大的匹配。

最小不相交路徑覆蓋:一張有向圖,用最少的鏈覆蓋所有的點,鏈之間不能有公共點。

將點和邊分別作為二分圖的兩邊,然後跑匹配,最小鏈覆蓋 = 原圖點數-最大匹配。

最小可相交路徑覆蓋:一張有向圖,用最少的鏈覆蓋所有的 點,鏈之間可以有公共點。

先跑一遍傳遞閉包,然後變成最小不相交路徑覆蓋。

補充

小黃鴨調試法

當你的程式碼出現問題的時候,

將小黃鴨想像成你的同學,

將你的程式碼一行一行地講給它,

也許講到一半你就知道問題出在哪了。

不要定義以下變數名

next,abs,x1,y1,size……

並非原創,僅是整理,請見諒