今天才寫了prim的堆優化,發現kruskal竟然比prim跑得快。。。css
迴歸正題:
如下是我我的對最小生成樹各類算法的理解,以及個人代碼。
如下我將點數稱爲n,邊數稱爲m;c++
Prim
算法過程(來自百度百科):
1. 輸入:一個加權連通圖,其中頂點集合爲V,邊集合爲E;
2. 初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;
3. 重複下列操做,直到Vnew = V:
- a.在集合E中選取權值最小的邊< u, v >,其中u爲集合Vnew中的元素,而v不在Vnew集合當中,而且v∈V(若是存在有多條知足前述條件即具備相同權值的邊,則可任意選取其中之一);
- b.將v加入集合Vnew中,將< u, v >邊加入集合Enew中;
4. 輸出:使用集合Vnew和Enew來描述所獲得的最小生成樹。
而後如下是我本身的解釋:
初始時任選一個起點,並加入點集;
不斷將離訪問過的點集最近的未訪問的點連邊(或者說選最短的邊),加入點集中,並將連邊加入邊集(或累加答案);
這樣進行n-1次,每次加入一個點和一條邊,結束後就會造成一棵n個節點,n-1條邊的樹;
正確性我也懶得證了,本身百度看吧。
時間複雜度:O(nm)
代碼就不用看了,這個複雜度徹底沒法接受,必須優化;算法
Prim–堆優化
對於上面的過程,最有優化餘地的一步是什麼呢?編程
不斷將離訪問過的點集最近的未訪問的點連邊(或者說選最短的邊)markdown
很明顯,粗體字部分能夠優化(通常來講,將求最小值或最大值從n優化至logn級別都是常見的優化);
具體方法就是將有可能擴展的邊都放入一個小根堆中,每次取最小的,這樣能夠將每次取最小值的過程優化至log級別的;(不知道堆的同窗能夠自行百度)
時間複雜度:低於O(nlogm)
手寫堆又會增長編程複雜度,咱們可使用c++自帶的優先隊列(priority_queue),這就是一個堆;less
priority_queue< T > (T爲類型)(後面括號內爲時間複雜度)
1. T top(void):返回堆頂(即最值);(1)
2. void pop(void):彈出堆頂;(logn)
3. void push(T):壓入新元素;(logn)
4. bool empty(void):若是堆爲空返回true,不然返回false;(1)
5. int size(void):返回堆的元素數量;(1)
另外一種定義方式:(建議)
priority_queue< T , vector< T > ,less< T > > (大根堆)
priority_queue< T , vector< T > ,greater< T > > (小根堆)
用自定義類型的前提是那個類型有定義小於號(大根堆)/ 大於號(小根堆)
關於比較符號定義能夠看個人代碼;優化
個人代碼:ui
//此題來自洛谷P3366 【模板】最小生成樹,是一道模板題,你們能夠去作一下。
//我這裏存邊用的是鄰接表,省空間,也方便;
//也能夠用vector來存邊
#include<bits/stdc++.h>
using namespace std;
struct edge{ //自定義邊類
int to,next,w;
bool operator > (const edge& y) const { //大於號
return w>y.w;
}
}e[400001];
int n,m,tot;
int head[5001]; //鄰接表用
int vis[5001]; //標記點是否訪問過
priority_queue<edge,vector<edge>,greater<edge> > q; //小根堆
void addedge(int x,int y,int l){ //加邊
tot++;
e[tot].to=y;
e[tot].next=head[x];
e[tot].w=l;
head[x]=tot;
}
int prim(){ //我這裏將1做爲起點
for(int i=head[1];i;i=e[i].next){ //將1的鄰邊都放入優先隊列
q.push(e[i]);
}
vis[1]=1;
int ans=0;
int left=n-1; //剩餘須要加邊數
while(left&&!q.empty()){
edge t=q.top();
q.pop();
if(vis[t.to]){
continue;
}
ans+=t.w;
left--;
int u=t.to;
vis[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(!vis[v]){
q.push(e[i]);
}
}
}
return ans;
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++){
int x,y,l;
scanf("%d %d %d",&x,&y,&l);
addedge(x,y,l);
addedge(y,x,l);
}
int ans=prim();
printf("%d",ans);
return 0;
}
Kruskal
算法過程(來自百度百科):
先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點當作各棵樹上的根結點,以後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不一樣的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取(會出現環),而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。
個人描述:
將邊排序後,從小到大隻要不造成環就加邊,直到加夠n-1條邊爲止。
正確性很顯然,能夠本身去看一下具體證實。
時間複雜度(暴力判環):低於O(m^2)
很明顯,判環這裏能夠用並查集優化到近似O(1),那麼算法的時間就都會集中在對邊進行排序上,時間也就是排序的時間;
時間複雜度(並查集判環):O(mlogm)
這樣的複雜度在稀疏圖優於Prim,稠密圖劣於Prim,本身酌情使用。
Kruskal的好處在於它極易編寫,而Prim編寫難度較高。spa
下來看代碼吧:.net
//這題也是洛谷P3366,你們能夠再寫一次Kruskal交一次。
#include<bits/stdc++.h>
#define inf 2000000000
using namespace std;
struct edge{
int a,b,w;
}e[200001];
int n,m;
int f[5001];
int sum=0;
int find(int x){ //並查集
return x==f[x]?x:f[x]=find(f[x]);
}
void add(int i,int x,int y,int l){
e[i].w=l;
e[i].a=x;
e[i].b=y;
}
int cmp(edge x,edge y){
return x.w<y.w;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
f[i]=i;
}
for(int i=1;i<=m;i++){
int x,y,l;
scanf("%d %d %d",&x,&y,&l);
add(i,x,y,l);
}
sort(e+1,e+m+1,cmp);
int tot=0;
//Kruskal的主體只有這一個循環
for(int i=1;i<=m&&tot<=n-1;i++){
if(find(e[i].a)!=find(e[i].b)){ //判環
sum+=e[i].w;
f[find(e[i].a)]=find(e[i].b);
tot++;
}
}
if(tot==n-1){
cout<<sum;
}
else{
cout<<"orz";
}
return 0;
}
交了以後不出意外應該是Kruskal略快於Prim,這是由於這題m較小。 不過,Kruskal通常狀況都足夠了,具體看數據範圍。 以上即是我對最小生成樹的總結。 end