圖論中的常見演算法分析比較和模板
圖論小結(一)
從一開始搞ACM到現在也有幾個年頭了,而搞圖論的時間可是從一開始搞ACM開始。所以,總是對圖論有著一種獨有的感情。圖論的內容說難不難,但是確實在演算法中日常生活中可以經常遇到,且一個很有趣的演算法。這也是我當初選擇圖論的原因,但是圖論的程式碼量一般都比較的大,當初就是看到別人啪啪的一下只就敲出了幾百行的圖論程式碼,而自己當時是佩服的不得了啊。可是進入圖論後,才發現,一照進圖論十年不想敲程式碼啊。。。。
廢話說太多了,其實圖論內容很多。今天,最要總結一下最短路和生成樹還有拓撲問題。還有一下更高深的網路流啊啥的一堆等下次再說。
一、最小生成樹
解決最小生成樹的問題,主要有兩個演算法。
1、Kruscal演算法
2、Prime演算法
Kruscal演算法百度百科的解釋及步驟:
求加權連通圖的最小生成樹的演算法。kruskal演算法總共選擇n- 1條邊,所使用的貪婪準則是:從剩下的邊中選擇一條不會產生環路的具有最小耗費的邊加入已選擇的邊的集合中。注意到所選取的邊若產生環路則不可能形成一棵生成樹。kruskal演算法分e 步,其中e 是網路中邊的數目。按耗費遞增的順序來考慮這e 條邊,每次考慮一條邊。當考慮某條邊時,若將其加入到已選邊的集合中會出現環路,則將其拋棄,否則,將它選入。
空間複雜度為O(N*N)
時間複雜度為O(N*logN)(根據排序的演算法時間決定)
Prim演算法百度百科的步驟:
在推薦一個生成樹講的話的部落格以供大家參考學習:部落格連結
int Prim()
{
int dis[N],ans = 0; //生成樹當前的最小花費,最終的最小花費
for(int i = 1;i <= n;++i){ //初始化
dis[i] = graph[1][i];
}
for(int i = 2;i <= n;++i){ //N個頂點要連通只須n-1條邊
int x = 1,m = INF;
for(int j = 2;j <= n;++j){ //根據貪心,找出當前最短的一條邊
if(dis[j] != -1&&dis[j] < m)
m = dis[x = j];
}
ans += m;
dis[x] = -1; //改點已經使用過,不可能在出現更小的情況
for(int j = 2;j <= n;++j){
/*
這個if就是prim演算法和Dijkstra演算法的本質不同之處
等提到Dijkstra演算法時候,會著重提出。進行比較區分
*/
if((dis[j]!=-1)&&(dis[j] > graph[x][j])) //更新點(當前j點的最短距離,跟從x到j那個更小)
dis[j] = graph[x][j];
}
}
return ans;
}
二、最短路演算法
最短路演算法最要使用的有三個Dijkstra演算法和Bellman-Ford以及Floyd演算法。而這三個演算法有的都有自己的優化程式,下面會一一講述。
Dijkstra演算法:
這個演算法是通過為每個頂點 v 保留目前為止所找到的從s到v的最短路徑來工作的。初始時,原點 s 的路徑長度值被賦為 0 (d[s] = 0),若存在能直接到達的邊(s,m),則把d[m]設為w(s,m),同時把所有其他(s不能直接到達的)頂點的路徑長度設為無窮大,即表示我們不知道任何通向這些頂點的路徑(對於 V 中所有頂點 v 除 s 和上述 m 外 d[v] = ∞)。當演算法退出時,d[v] 中儲存的便是從 s 到 v 的最短路徑,或者如果路徑不存在的話是無窮大。 Dijkstra 演算法的基礎操作是邊的擴充:如果存在一條從 u 到 v 的邊,那麼從 s 到 v 的最短路徑可以通過將邊(u, v)新增到尾部來擴充一條從 s 到 v 的路徑。這條路徑的長度是 d[u] + w(u, v)。如果這個值比目前已知的 d[v] 的值要小,我們可以用新值來替代當前 d[v] 中的值。擴充邊的操作一直執行到所有的 d[v] 都代表從 s 到 v 最短路徑的花費。這個演算法經過組織因而當 d[u] 達到它最終的值的時候每條邊(u, v)都只被擴充一次。
時間複雜度我O(|V^2|)
但是可以優化到O(n*logn)優化程式可以看我以前寫的文章:文章連結
function Dijkstra(G, w, s)
for each vertex v in V[G] // 初始化
d[v] := infinity // 將各點的已知最短距離先設成無窮大
previous[v] := undefined // 各點的已知最短路徑上的前趨都未知
d[s] := 0 // 因為出發點到出發點間不需移動任何距離,所以可以直接將s到s的最小距離設為0
S := empty set
Q := set of all vertices
while Q is not an empty set // Dijkstra演演算法主體
u := Extract_Min(Q)
S.append(u)
for each edge outgoing from u as (u,v)
if d[v] > d[u] + w(u,v) // 擴充邊(u,v)。w(u,v)為從u到v的路徑長度。
d[v] := d[u] + w(u,v) // 更新路徑長度到更小的那個和值。
previous[v] := u // 紀錄前趨頂點
現在就說說剛才在Prim演算法中提到了但是沒有解釋的那行程式吧。
Prim更新:if(d[y] != -1&&d[y]>graph[x][y]) d[y] = graph[x][y]; //d[i]==-1表示改點已經更新過
Dijkstra更新:if(d[y]>d[x]+w[x][y]) d[y] = d[x]+w[x][y];
為什麼會有這個區別呢?其實,只要你對這兩個演算法的本質理解了,你就自然會知道了。上面我們已經提到了Prim是求解最小生成樹的,其終極目標是使得一個圖連通且最終花費最小。而Dijkstra是求單源最短路的。即,給你一個起始點,叫你求出從這個起始點到各點或者給定目標點的最短距離。所以,我們在為Prim更新的時候考慮的是最終總的花費最小,而考慮Dijkstra是考慮起點到當前更新點的花費最小。
所以,最終我得到的d[]也是有區別的。prim的d[]是表示當前連通改點最小的花費d[],而Dijkstra表示的是從起點到當前更新點的最小花費是d[].
圖雖然搓了點,但是卻可以很好的解釋他們兩個的區別。其實,是想畫無向圖的,就湊合著看吧。
如果,是prim的話是選擇(1,2)+(2,3);而Dijkstra的話則是更新(1,3)(假設起點為1).現在應該知道他他們的區別了吧。
Bellman-Ford演算法維基百科解釋:
它的原理是對圖進行V-1次鬆弛操作,得到所有可能的最短路徑。其優於迪科斯徹演算法的方面是邊的權值可以為負數、實現簡單,缺點是時間複雜度過高,高達O(VE)。但演算法可以進行若干種優化,提高了效率。
貝爾曼-福特演算法與迪科斯徹演算法類似,都以鬆弛操作為基礎,即估計的最短路徑值漸漸地被更加準確的值替代,直至得到最優解。在兩個演算法中,計算時每個邊之間的估計距離值都比真實值大,並且被新找到路徑的最小長度替代。 然而,迪科斯徹演算法以貪心法選取未被處理的具有最小權值得節點,然後對其的出邊進行鬆弛操作;而貝爾曼-福特演算法簡單地對所有邊進行鬆弛操作,共|E | − 1次,其中 |E |是圖的邊的數量。在重複地計算中,已計算得到正確的距離的邊的數量不斷增加,直到所有邊都計算得到了正確的路徑。這樣的策略使得貝爾曼-福特演算法比迪科斯徹演算法適用於更多種類的輸入。
貝爾曼-福特演算法的最多執行O(|V|·|E|)次,|V|和|E|分別是節點和邊的數量)。
procedure BellmanFord(list vertices, list edges, vertex source)
// 該實現讀入邊和節點的列表,並向兩個陣列(distance和predecessor)中寫入最短路徑資訊
// 步驟1:初始化圖
for each vertex v in vertices:
if v is source then distance[v] := 0
else distance[v] := infinity
predecessor[v] := null
// 步驟2:重複對每一條邊進行鬆弛操作
for i from 1 to size(vertices)-1:
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
distance[v] := distance[u] + w
predecessor[v] := u
// 步驟3:檢查負權環
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
error "圖包含了負權環"
改演算法同樣也有一個很常用的優化演算法SPFA演算法,就是將檢查的時候常常用FIFO來代替了迴圈檢查。
/*
Head[]為鄰接表的表頭
Key[] 為鄰接表的頂點
Next[]為鄰接表的下個節點
w[] 為該點的權重
*/
void SPFA(int s) //s 為起點
{
queue<int> q;
int c[MAXV]; //判斷是否有負環
bool inq[MAXV]; //在佇列中的標記
for(int i = 0;i < n;++i)
d[i] = (i==s?0:INF);
memset(inq,0,sizeof(inq));
q.push(s);
while(!q.empty())
{
int x = q.front();
q.pop();
inq[x] = 0; //清除在佇列中的標記
for(int e = Head[x];e != -1;e = Next[e])if(d[Key[e]] > d[x]+w[e]){
d[Key[e]] = d[x] + w[e];
if(!inq[Key[e]]){ //如果已經在佇列中,就不要重複加了
inq[Key[e]] = 1;
c[Key[e]]++;
if(c[Key[e]] > MaxVerter)
RETURN "有負環"
q.push(Key[e]);
}
}
}
}
判斷有無負環:如果某個點進入佇列的次數超過N次則存在負環(SPFA無法處理帶負環的圖)
相關文章
- 演算法題常見模板演算法
- IocPerformance 常見IOC 功能、效能比較ORM
- 常見名詞的用法與比較【***】 02天
- 深度比較常見庫中序列化和反序列化效能的效能差異
- 常見分散式協議和演算法的說明和對比分散式協議演算法
- 機器學習常見演算法效能比較與調參建議機器學習演算法
- 常見的排序演算法分析(一)排序演算法
- React與Vue模板使用比較(一、vue模板與React JSX比較)ReactVueJS
- 126 PHP目前比較常見的五大執行模式PHP模式
- 常見的web系統測試管理工具比較Web
- 常見的Linux中介軟體有哪些?哪個比較好用?Linux
- 模板 - 圖論圖論
- 【模板】圖論圖論
- powershell中的where和foreach比較
- Mysql中的Datetime和Timestamp比較MySql
- 粒子群演算法和遺傳演算法的比較演算法
- 3 個 Python 模板庫比較Python
- 論PHP常見的漏洞PHP
- Java 中 Comparable 和 Comparator 比較Java
- 比較 Pandas、Polars 和 PySpark:基準分析Spark
- MySQL 中的 distinct 和 group by 的效能比較MySql
- Lock的獨佔鎖和共享鎖的比較分析
- Linux常見問題彙總,比較適合菜鳥哈Linux
- js 深比較和淺比較JS
- 【原創】InnoDB 和TokuDB的讀寫分析與比較
- Go和Python比較的話,哪個比較好?GoPython
- [圖解] 機器學習常見的基本演算法圖解機器學習演算法
- ==和equals方法的比較
- ImageMagic 和 GraphicsMagick 的比較
- ArrayList和LinkedList的比較
- CAD中圖紙比較功能怎麼用
- 演算法:比較含退格的字串演算法字串
- Flutter 常見異常分析Flutter
- Oracle date 型別比較和String比較Oracle型別
- JAVA中字串比較equals()和equalsIgnoreCase()的區別Java字串
- [pythonskill]Python中NaN和None的詳細比較PythonNaNNone
- JAVA中的Comparable介面和自定義比較器Java
- Java中的字串操作(比較String,StringBuiler和StringBuffer)Java字串UI
- numpy 中 array 和 matrix 相乘的結果比較