0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文詳細(xì)了解Prim最小生成樹算法

算法與數(shù)據(jù)結(jié)構(gòu) ? 來源:labuladong ? 作者:labuladong ? 2022-03-16 15:27 ? 次閱讀

讀完本文,可以去力扣解決如下題目:

1135. 最低成本聯(lián)通所有城市(中等1584. 連接所有點的最小費用(中等

本文是第 7 篇圖論算法文章,先列舉一下我之前寫過的圖論算法:

1、圖論算法基礎(chǔ)

2、二分圖判定算法

3、環(huán)檢測和拓?fù)渑判蛩惴?/span>

4、Dijkstra 最短路徑算法

5、Union Find 并查集算法

6、Kruskal 最小生成樹算法

像圖論算法這種高級算法雖然不算難,但是閱讀量普遍比較低,我本來是不想寫 Prim 算法的,但考慮到算法知識結(jié)構(gòu)的完整性,我還是想把 Prim 算法的坑填上,這樣所有經(jīng)典的圖論算法就基本完善了。

Prim 算法和 Kruskal 算法都是經(jīng)典的最小生成樹算法,閱讀本文之前,希望你讀過前文Kruskal 最小生成樹算法,了解最小生成樹的基本定義以及 Kruskal 算法的基本原理,這樣就能很容易理解 Prim 算法邏輯了。

對比 Kruskal 算法

圖論的最小生成樹問題,就是讓你從圖中找若干邊形成一個邊的集合mst,這些邊有以下特性:

1、這些邊組成的是一棵樹(樹和圖的區(qū)別在于不能包含環(huán))。

2、這些邊形成的樹要包含所有節(jié)點。

3、這些邊的權(quán)重之和要盡可能小。

那么 Kruskal 算法是使用什么邏輯滿足上述條件,計算最小生成樹的呢?

首先,Kruskal 算法用到了貪心思想,來滿足權(quán)重之和盡可能小的問題:

先對所有邊按照權(quán)重從小到大排序,從權(quán)重最小的邊開始,選擇合適的邊加入mst集合,這樣挑出來的邊組成的樹就是權(quán)重和最小的。

其次,Kruskal 算法用到了Union-Find 并查集算法,來保證挑選出來的這些邊組成的一定是一棵「樹」,而不會包含環(huán)或者形成一片「森林」:

如果一條邊的兩個節(jié)點已經(jīng)是連通的,則這條邊會使樹中出現(xiàn)環(huán);如果最后的連通分量總數(shù)大于 1,則說明形成的是「森林」而不是一棵「樹」。

那么,本文的主角 Prim 算法是使用什么邏輯來計算最小生成樹的呢?

首先,Prim 算法也使用貪心思想來讓生成樹的權(quán)重盡可能小,也就是「切分定理」,這個后文會詳細(xì)解釋。

其次,Prim 算法使用BFS 算法思想visited布爾數(shù)組避免成環(huán),來保證選出來的邊最終形成的一定是一棵樹。

Prim 算法不需要事先對所有邊排序,而是利用優(yōu)先級隊列動態(tài)實現(xiàn)排序的效果,所以我覺得 Prim 算法類似于 Kruskal 的動態(tài)過程。

下面介紹一下 Prim 算法的核心原理:切分定理

切分定理

「切分」這個術(shù)語其實很好理解,就是將一幅圖分為兩個不重疊且非空的節(jié)點集合:

9d3d559a-93a9-11ec-952b-dac502259ad0.jpg

紅色的這一刀把圖中的節(jié)點分成了兩個集合,就是一種「切分」,其中被紅線切中的的邊(標(biāo)記為藍色)叫做「橫切邊」。

PS:記住這兩個專業(yè)術(shù)語的意思,后面我們會頻繁使用這兩個詞,別搞混了。

當(dāng)然,一幅圖肯定可以有若干種切分,因為根據(jù)切分的定義,只要你能一刀把節(jié)點分成兩部分就行。

接下來我們引入「切分定理」:

對于任意一種「切分」,其中權(quán)重最小的那條「橫切邊」一定是構(gòu)成最小生成樹的一條邊。

這應(yīng)該很容易證明,如果一幅加權(quán)無向圖存在最小生成樹,假設(shè)下圖中用綠色標(biāo)出來的邊就是最小生成樹:

9d584df0-93a9-11ec-952b-dac502259ad0.jpg

那么,你肯定可以找到若干「切分」方式,將這棵最小生成樹切成兩棵子樹。比如下面這種切分:

9d801e48-93a9-11ec-952b-dac502259ad0.jpg

你會發(fā)現(xiàn),任選一條藍色的「橫切邊」都可以將這兩棵子樹連接起來,構(gòu)成一棵生成樹。

那么為了讓最終這棵生成樹的權(quán)重和最小,你說你要怎么選?

肯定選權(quán)重最小的那條「橫切邊」對吧,這就證明了切分定理。

關(guān)于切分定理,你也可以用反證法證明:

給定一幅圖的最小生成樹,那么隨便給一種「切分」,一定至少有一條「橫切邊」屬于最小生成樹。

假設(shè)這條「橫切邊」不是權(quán)重最小的,那說明最小生成樹的權(quán)重和就還有再減小的余地,那這就矛盾了,最小生成樹的權(quán)重和本來就是最小的,怎么再減?所以切分定理是正確的。

有了這個切分定理,你大概就有了一個計算最小生成樹的算法思路了:

既然每一次「切分」一定可以找到最小生成樹中的一條邊,那我就隨便切唄,每次都把權(quán)重最小的「橫切邊」拿出來加入最小生成樹,直到把構(gòu)成最小生成樹的所有邊都切出來為止

嗯,可以說這就是 Prim 算法的核心思路,不過具體實現(xiàn)起來,還是要有些技巧的。

因為你沒辦法讓計算機理解什么叫「隨便切」,所以應(yīng)該設(shè)計機械化的規(guī)則和章法來調(diào)教你的算法,并盡量減少無用功。

Prim 算法實現(xiàn)

我們思考算法問題時,如果問題的一般情況不好解決,可以從比較簡單的特殊情況入手,Prim 算法就是使用的這種思路。

按照「切分」的定義,只要把圖中的節(jié)點切成兩個不重疊且非空的節(jié)點集合即可算作一個合法的「切分」,那么我只切出來一個節(jié)點,是不是也算是一個合法的「切分」?

是的,這是最簡單的「切分」,而且「橫切邊」也很好確定,就是這個節(jié)點的邊。

那我們就隨便選一個點,假設(shè)就從A點開始切分:

9d92ff36-93a9-11ec-952b-dac502259ad0.jpg

既然這是一個合法的「切分」,那么按照切分定理,這些「橫切邊」AB, AF中權(quán)重最小的邊一定是最小生成樹中的一條邊:

9da48c06-93a9-11ec-952b-dac502259ad0.jpg

好,現(xiàn)在已經(jīng)找到最小生成樹的第一條邊(邊AB),然后呢,如何安排下一次「切分」?

按照 Prim 算法的邏輯,我們接下來可以圍繞AB這兩個節(jié)點做切分:

9dbabdaa-93a9-11ec-952b-dac502259ad0.jpg

然后又可以從這個切分產(chǎn)生的橫切邊(圖中藍色的邊)中找出權(quán)重最小的一條邊,也就又找到了最小生成樹中的第二條邊BC

9dc9a5ea-93a9-11ec-952b-dac502259ad0.jpg

接下來呢?也是類似的,再圍繞著A, B, C這三個點做切分,產(chǎn)生的橫切邊中權(quán)重最小的邊是BD,那么BD就是最小生成樹的第三條邊:

9de1d958-93a9-11ec-952b-dac502259ad0.jpg

接下來再圍繞A, B, C, D這四個點做切分……

Prim 算法的邏輯就是這樣,每次切分都能找到最小生成樹的一條邊,然后又可以進行新一輪切分,直到找到最小生成樹的所有邊為止。

這樣設(shè)計算法有一個好處,就是比較容易確定每次新的「切分」所產(chǎn)生的「橫切邊」。

比如回顧剛才的圖,當(dāng)我知道了節(jié)點A, B的所有「橫切邊」(不妨表示為cut({A, B})),也就是圖中藍色的邊:

9dbabdaa-93a9-11ec-952b-dac502259ad0.jpg

是否可以快速算出cut({A, B, C}),也就是節(jié)點A, B, C的所有「橫切邊」有哪些?

9de1d958-93a9-11ec-952b-dac502259ad0.jpg

是可以的,因為我們發(fā)現(xiàn):

cut({A,B,C})=cut({A,B})+cut({C})

cut({C})就是節(jié)點C的所有鄰邊:

9e186f5e-93a9-11ec-952b-dac502259ad0.jpg

這個特點使我們用我們寫代碼實現(xiàn)「切分」和處理「橫切邊」成為可能:

在進行切分的過程中,我們只要不斷把新節(jié)點的鄰邊加入橫切邊集合,就可以得到新的切分的所有橫切邊。

當(dāng)然,細(xì)心的讀者肯定發(fā)現(xiàn)了,cut({A, B})的橫切邊和cut({C})的橫切邊中BC邊重復(fù)了。

不過這很好處理,用一個布爾數(shù)組inMST輔助,防止重復(fù)計算橫切邊就行了。

最后一個問題,我們求橫切邊的目的是找權(quán)重最小的橫切邊,怎么做到呢?

很簡單,用一個優(yōu)先級隊列存儲這些橫切邊,就可以動態(tài)計算權(quán)重最小的橫切邊了。

明白了上述算法原理,下面來看一下 Prim 算法的代碼實現(xiàn)

classPrim{
//核心數(shù)據(jù)結(jié)構(gòu),存儲「橫切邊」的優(yōu)先級隊列
privatePriorityQueue<int[]>pq;
//類似visited數(shù)組的作用,記錄哪些節(jié)點已經(jīng)成為最小生成樹的一部分
privateboolean[]inMST;
//記錄最小生成樹的權(quán)重和
privateintweightSum=0;
//graph是用鄰接表表示的一幅圖,
//graph[s]記錄節(jié)點s所有相鄰的邊,
//三元組int[]{from,to,weight}表示一條邊
privateList<int[]>[]graph;

publicPrim(List<int[]>[]graph){
this.graph=graph;
this.pq=newPriorityQueue<>((a,b)->{
//按照邊的權(quán)重從小到大排序
returna[2]-b[2];
});
//圖中有n個節(jié)點
intn=graph.length;
this.inMST=newboolean[n];

//隨便從一個點開始切分都可以,我們不妨從節(jié)點0開始
inMST[0]=true;
cut(0);
//不斷進行切分,向最小生成樹中添加邊
while(!pq.isEmpty()){
int[]edge=pq.poll();
intto=edge[1];
intweight=edge[2];
if(inMST[to]){
//節(jié)點to已經(jīng)在最小生成樹中,跳過
//否則這條邊會產(chǎn)生環(huán)
continue;
}
//將邊edge加入最小生成樹
weightSum+=weight;
inMST[to]=true;
//節(jié)點to加入后,進行新一輪切分,會產(chǎn)生更多橫切邊
cut(to);
}
}

//將s的橫切邊加入優(yōu)先隊列
privatevoidcut(ints){
//遍歷s的鄰邊
for(int[]edge:graph[s]){
intto=edge[1];
if(inMST[to]){
//相鄰接點to已經(jīng)在最小生成樹中,跳過
//否則這條邊會產(chǎn)生環(huán)
continue;
}
//加入橫切邊隊列
pq.offer(edge);
}
}

//最小生成樹的權(quán)重和
publicintweightSum(){
returnweightSum;
}

//判斷最小生成樹是否包含圖中的所有節(jié)點
publicbooleanallConnected(){
for(inti=0;iif(!inMST[i]){
returnfalse;
}
}
returntrue;
}
}

明白了切分定理,加上詳細(xì)的代碼注釋,你應(yīng)該能夠看懂 Prim 算法的代碼了。

這里我們可以再回顧一下本文開頭說的 Prim 算法和Kruskal 算法的聯(lián)系:

Kruskal 算法是在一開始的時候就把所有的邊排序,然后從權(quán)重最小的邊開始挑選屬于最小生成樹的邊,組建最小生成樹。

Prim 算法是從一個起點的切分(一組橫切邊)開始執(zhí)行類似 BFS 算法的邏輯,借助切分定理和優(yōu)先級隊列動態(tài)排序的特性,從這個起點「生長」出一棵最小生成樹。

說到這里,Prim 算法的時間復(fù)雜度是多少呢

這個不難分析,復(fù)雜度主要在優(yōu)先級隊列pq的操作上,由于pq里面裝的是圖中的「邊」,假設(shè)一幅圖邊的條數(shù)為E,那么最多操作O(E)pq。每次操作優(yōu)先級隊列的時間復(fù)雜度取決于隊列中的元素個數(shù),取最壞情況就是O(logE)。

所以這種 Prim 算法實現(xiàn)的總時間復(fù)雜度是O(ElogE)?;叵胍幌?/span>Kruskal 算法,它的時間復(fù)雜度主要是給所有邊按照權(quán)重排序,也是O(ElogE)。

不過話說回來,和前文Dijkstra 算法類似,Prim 算法的時間復(fù)雜度也是可以優(yōu)化的,但優(yōu)化點在于優(yōu)先級隊列的實現(xiàn)上,和 Prim 算法本身的算法思想關(guān)系不大,所以我們這里就不做討論了,有興趣的讀者可以自行搜索。

接下來,我們實操一波,把之前用 Kruskal 算法解決的力扣題目運用 Prim 算法再解決一遍。

題目實踐

第一題是力扣第 1135 題「最低成本聯(lián)通所有城市」,這是一道標(biāo)準(zhǔn)的最小生成樹問題:

9e2d4276-93a9-11ec-952b-dac502259ad0.png

函數(shù)簽名如下:

intminimumCost(intn,int[][]connections);

每座城市相當(dāng)于圖中的節(jié)點,連通城市的成本相當(dāng)于邊的權(quán)重,連通所有城市的最小成本即是最小生成樹的權(quán)重之和。

那么解法就很明顯了,我們先把題目輸入的connections轉(zhuǎn)化成鄰接表形式,然后輸入給之前實現(xiàn)的Prim算法類即可:

publicintminimumCost(intn,int[][]connections){
//轉(zhuǎn)化成無向圖鄰接表的形式
List<int[]>[]graph=buildGraph(n,connections);
//執(zhí)行Prim算法
Primprim=newPrim(graph);

if(!prim.allConnected()){
//最小生成樹無法覆蓋所有節(jié)點
return-1;
}

returnprim.weightSum();
}

List<int[]>[]buildGraph(intn,int[][]connections){
//圖中共有n個節(jié)點
List<int[]>[]graph=newLinkedList[n];
for(inti=0;inewLinkedList<>();
}
for(int[]conn:connections){
//題目給的節(jié)點編號是從1開始的,
//但我們實現(xiàn)的Prim算法需要從0開始編號
intu=conn[0]-1;
intv=conn[1]-1;
intweight=conn[2];
//「無向圖」其實就是「雙向圖」
//一條邊表示為int[]{from,to,weight}
graph[u].add(newint[]{u,v,weight});
graph[v].add(newint[]{v,u,weight});
}
returngraph;
}

classPrim{/*見上文*/}

關(guān)于buildGraph函數(shù)需要注意兩點:

一是題目給的節(jié)點編號是從 1 開始的,所以我們做一下索引偏移,轉(zhuǎn)化成從 0 開始以便Prim類使用;

二是如何用鄰接表表示無向加權(quán)圖,前文圖論算法基礎(chǔ)說過「無向圖」其實就可以理解為「雙向圖」。

這樣,我們轉(zhuǎn)化出來的graph形式就和之前的Prim算法類對應(yīng)了,可以直接施展 Prim 算法計算最小生成樹。

再來看看力扣第 1584 題「連接所有點的最小費用」:

9e49b80c-93a9-11ec-952b-dac502259ad0.png

比如題目給的例子:

points=[[0,0],[2,2],[3,10],[5,2],[7,0]]

算法應(yīng)該返回 20,按如下方式連通各點:

9e5e5dde-93a9-11ec-952b-dac502259ad0.png

函數(shù)簽名如下:

intminCostConnectPoints(int[][]points);

很顯然這也是一個標(biāo)準(zhǔn)的最小生成樹問題:每個點就是無向加權(quán)圖中的節(jié)點,邊的權(quán)重就是曼哈頓距離,連接所有點的最小費用就是最小生成樹的權(quán)重和。

所以我們只要把points數(shù)組轉(zhuǎn)化成鄰接表的形式,即可復(fù)用之前實現(xiàn)的Prim算法類:

publicintminCostConnectPoints(int[][]points){
intn=points.length;
List<int[]>[]graph=buildGraph(n,points);
returnnewPrim(graph).weightSum();
}

//構(gòu)造無向圖
List<int[]>[]buildGraph(intn,int[][]points){
List<int[]>[]graph=newLinkedList[n];
for(inti=0;inewLinkedList<>();
}
//生成所有邊及權(quán)重
for(inti=0;ifor(intj=i+1;jintxi=points[i][0],yi=points[i][1];
intxj=points[j][0],yj=points[j][1];
intweight=Math.abs(xi-xj)+Math.abs(yi-yj);
//用points中的索引表示坐標(biāo)點
graph[i].add(newint[]{i,j,weight});
graph[j].add(newint[]{j,i,weight});
}
}
returngraph;
}

classPrim{/*見上文*/}

這道題做了一個小的變通:每個坐標(biāo)點是一個二元組,那么按理說應(yīng)該用五元組表示一條帶權(quán)重的邊,但這樣的話不便執(zhí)行 Prim 算法;所以我們用points數(shù)組中的索引代表每個坐標(biāo)點,這樣就可以直接復(fù)用之前的 Prim 算法邏輯了。

到這里,Prim 算法就講完了,整個圖論算法也整的差不多了,更多精彩文章,敬請期待。

原文標(biāo)題:Prim 算法,YYDS

文章出處:【微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
審核編輯:湯梓紅


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 算法
    +關(guān)注

    關(guān)注

    23

    文章

    4587

    瀏覽量

    92503
  • 計算
    +關(guān)注

    關(guān)注

    2

    文章

    442

    瀏覽量

    38704
  • 檢測
    +關(guān)注

    關(guān)注

    5

    文章

    4413

    瀏覽量

    91305

原文標(biāo)題:Prim 算法,YYDS

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    算法設(shè)計與分析:10.1 最小生成Prim算法(2)(2)#硬聲創(chuàng)作季

    算法設(shè)計
    學(xué)習(xí)電子
    發(fā)布于 :2022年12月21日 02:45:14

    算法設(shè)計與分析:10.1 最小生成Prim算法(2)(3)#硬聲創(chuàng)作季

    算法設(shè)計
    學(xué)習(xí)電子
    發(fā)布于 :2022年12月21日 02:45:41

    算法設(shè)計與分析:10.1 最小生成Prim算法(3)(1)#硬聲創(chuàng)作季

    算法設(shè)計
    學(xué)習(xí)電子
    發(fā)布于 :2022年12月21日 02:46:08

    算法設(shè)計與分析:10.1 最小生成Prim算法(4)(1)#硬聲創(chuàng)作季

    算法設(shè)計
    學(xué)習(xí)電子
    發(fā)布于 :2022年12月21日 02:49:12

    matlab經(jīng)典算法的程序集合

    │???│?├─隨機數(shù)的產(chǎn)生│???│?├─最大流和最小截│???│?├─最短路和次短路│???│?├─最短路徑│???│?└─最小生成Prim
    發(fā)表于 11-14 12:05

    依存句法分析器的簡單實現(xiàn)

    最小生成即可。 最小生成 關(guān)于最小生成Prim
    發(fā)表于 10-17 13:12

    了解下STM32的時鐘

    的時鐘頻率又是如何確定的呢?帶著這個問題,我們詳細(xì)了解下STM32的時鐘。時鐘了解S
    發(fā)表于 08-06 07:11

    基于最小生成的層次K_means聚類算法

    基于最小生成的層次K_means聚類算法_賈瑞玉
    發(fā)表于 01-03 15:24 ?5次下載

    基于故障最小割集求解算法

    ,其主要缺點在于算法在時間和空間上的消耗嚴(yán)重依賴良好的變量順序。為了減少存儲資源并加快求解速度,提出了種基于可滿足性問題的故障最小割集求解算法
    發(fā)表于 11-21 16:05 ?10次下載
    基于故障<b class='flag-5'>樹</b><b class='flag-5'>最小</b>割集求解<b class='flag-5'>算法</b>

    基于Prim初始種群選取優(yōu)化遺傳算法的三維片上網(wǎng)絡(luò)低功耗映射

    針對將計算任務(wù)合理地映射到三維片上網(wǎng)絡(luò)( NoC)的問題,提出了種基于遺傳算法(GA)的改進算法。GA具有快速隨機的搜索能力,Prim算法
    發(fā)表于 12-07 14:40 ?0次下載
    基于<b class='flag-5'>Prim</b>初始種群選取優(yōu)化遺傳<b class='flag-5'>算法</b>的三維片上網(wǎng)絡(luò)低功耗映射

    Prim算法以及優(yōu)化實現(xiàn)

    最小生成(Minimum Spanning Trees),簡稱MST。是圖論中個非常重要的概念。解決這個問題有兩種算法,今天暫且先來討論
    的頭像 發(fā)表于 03-31 10:32 ?9434次閱讀
    <b class='flag-5'>Prim</b><b class='flag-5'>算法</b>以及優(yōu)化實現(xiàn)

    如何使用最優(yōu)最小生成進行三維模型形狀優(yōu)化方法的詳細(xì)資料說明

    針對海量、異構(gòu)、 復(fù)雜的三維模型高效形狀分析需求,提出基于最優(yōu)最小生成的三維模型形狀優(yōu)化方法。首先基于三維模型最小生成(3D-MST)構(gòu)造模型的結(jié)構(gòu)描述;其次通過拓?fù)浣Y(jié)構(gòu)與幾何形狀
    發(fā)表于 04-04 15:32 ?17次下載
    如何使用最優(yōu)<b class='flag-5'>最小生成</b><b class='flag-5'>樹</b>進行三維模型形狀優(yōu)化方法的<b class='flag-5'>詳細(xì)</b>資料說明

    MATLAB建模算法和程序資料合集免費下載

    本文檔的主要內(nèi)容詳細(xì)介紹的是MATLAB建模算法和程序資料合集免費下載包括了:遞推關(guān)系式的作圖程序,頂點覆蓋近似算法,方程求根,哈密爾頓回路,最小生成
    發(fā)表于 04-23 08:00 ?4次下載
    MATLAB建模<b class='flag-5'>算法</b>和程序資料合集免費下載

    數(shù)據(jù)結(jié)構(gòu)與算法中什么是最小生成

    ? 前言 在數(shù)據(jù)結(jié)構(gòu)與算法的 圖論 中,(生成)最小生成算法種常用并且和生活貼切比較近的
    的頭像 發(fā)表于 10-28 17:13 ?1982次閱讀

    解析并查集(Union-Find)算法原理

    并查集(Union-Find)算法個專門針對「動態(tài)連通性」的算法,我之前寫過兩次,因為這個算法的考察頻率高,而且它也是最小生成
    發(fā)表于 03-24 18:22 ?666次閱讀