圖論算法(五)最小生成樹Prim算法

最小生成樹\(Prim\)算法

咱們一般求最小生成樹有兩種常見的算法——\(Prim\)\(Kruskal\)算法,今天先總結最小生成樹概念和比較簡單的\(Prim\)算法ios

Part 1:最小生成樹基礎理論

定義

一個有 \(n\) 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的全部 \(n\) 個結點,而且有保持圖連通的最少的邊。
——來自百度百科算法

咱們用比較通俗的語言來說:(百度百科的解釋實在是太鬼了,我這個明白人都看着迷糊)數組

給定一張包含\(n\)個點\(m\)條邊的連通帶權無向\(G\),咱們從中選出\(n-1\)條邊,使得這\(n\)個點互相連通優化

連通以後,發現所選的\(n-1\)條邊和圖中的\(n\)個點,構成了一棵樹,咱們稱之爲:「生成樹」spa

咱們對這棵生成樹的全部邊權求和,獲得一個數\(size\),稱之爲:「生成樹大小」code

如今咱們就能夠字面上理解「最小生成樹」是什麼意思了——一個圖的全部生成樹中\(size\)值最小的那一個,就是這個圖的最小生成樹(\(Minimum\) \(Spanning\) \(Tree\),簡稱\(MST\)blog

其實你能夠更簡單的理解爲:最小生成樹就是連通\(n\)個點所花費的最小代價圖片

如今我將展現個人畫圖技巧,舉一個例子:ip

圖片爆炸了OvO
如今給定這張無向圖\(G\),咱們的任務是求出它的\(MST\)內存

使用肉眼觀察法,咱們獲得的\(MST\)應該是選擇\((1,3),(4,3),(2,4)\)這三條邊,權值和爲\(5\),也就是咱們只花了\(5\)的代價,就把全部點連起來了

經過枚舉全部生成樹能夠發現:不論怎麼選,上述方案的權值和必定是最小的,因此它的最小生成樹大小爲\(5\),包含\((1,3),(4,3),(2,4)\)這三條邊

定理

定理:任意一棵最小生成樹必定包含無向圖中權值最小的邊

定理證實:

反證法,假設無向圖\(G\)的最小生成樹不包含這個權值最小的邊(把這個最小權值邊設爲\(e\)\(e\)連通點\(x,y\),權值爲\(z\)

\(e\)添加到不包含\(e\)的這棵最小生成樹裏去,必定會造成一個環,而且環上的每一條\(e\)之外的邊的權值必定比\(z\)

此時,咱們隨便去掉一條\(e\)之外的邊,整個圖仍然連通成一棵樹,且權值和\(size\)更小(由於加入\(z\),去掉一個大於\(z\)的權)

發現這與一開始的假設矛盾,因此假設不成立,原命題成立,證畢。

Part 2:\(Prim\)算法

\(Prim\)算法原理

這裏咱們拋開正確性證實,只談原理(正確性證實是計算機科學家的事,咱們須要的是瞭解與應用)

最初,\(Prim\)算法僅肯定\(1\)號節點已經在最小生成樹中

\(一、\)設已經選入最小生成樹的節點集合爲\(T\),沒有選入的節點集合爲\(S\)

\(二、\)找到一條邊\(e(x,y,z)\)(鏈接\(x,y\),權值爲\(z\)),使得\(x\in S,y\in T\)\(z\)最小

\(三、\)在集合\(S\)中刪除\(y\),加入到集合\(T\)中,並累加\(z\)\(size\)

\(四、\)重複上述操做,直到集合\(S\)爲空爲止,此時\(size\)就是最小生成樹的大小

具體到代碼裏能夠這麼寫:

維護一個數組\(dis\),若節點\(i\in S\)\(dis[i]\)表示節點\(i\)與集合\(T\)中的節點之間權值最小的邊的權值,若\(i\in T\)\(dis[i]\)表示\(i\)被加入\(T\)時選出的最小邊的權值

發現這好像與\(dijkstra\)算法要維護的東西有點像:

\(dijkstra\)算法維護一個未知最短路的點到已知最短路的點的最短距離,每次肯定到達一個點的最短路,用於更新其餘未知點到已知點的最短距離

\(Prim\)算法維護一個未加入最小生成樹的點到已加入最小生成樹的點的邊權最小值,每次選擇一個點加入到最小生成樹,更新邊權最小值

因此咱們能夠用一個數組\(vis\)來標記一個節點是否屬於集合\(S\),每次從未標記的節點中選出\(dis\)值最小的,把它標記,加入集合\(T\),掃描這個點的全部出邊,更新另外一個端點的\(dis\)

最後,當集合\(S\)爲空時,算法結束,最小生成樹大小爲\(\sum_{x=2}^{n}dis[x]\),你也能夠直接在選出邊權最小值的時候直接累加,再也不求和

另外,\(Prim\)算法的時間複雜度爲\(O(n^2)\),算不上太優秀,可是由於有求最小值的操做,因此咱們能夠把\(dis\)數組換成一個小根堆,把時間複雜度優化到\(O(mlogn)\)

\(Code\) \(O(n^2)\)

#include<cstring>
#include<cstdio>
#define N 5010
using namespace std;
int f[N][N],dis[N],vis[N],m,n,total;
void prim(int x){
	memset(dis,0x7f,sizeof(dis));//賦初值無窮大
	memset(vis,1,sizeof(vis));//把全部點標記爲在集合S中
	dis[x]=0;//1號點dis爲0
	for(int i=1;i<=n;i++){
		int k=0;
		for(int j=1;j<=n;j++)
			if(vis[j]!=0&&(dis[j]<dis[k])) k=j;//集合S中最小的dis值的點爲k
		vis[k]=0;//把k加入到最小生成樹
		total+=dis[k];//把選擇邊的邊權累加到total裏
		for(int j=1;j<=n;j++)//用節點k更新dis中其餘的值
			if(vis[j]!=0&&(f[k][j]<dis[j])) dis[j]=f[k][j];//更新一個不在最小生成樹中的點j的dis值
	}
}
int main(){
	scanf("%d%d",&n,&m);
	memset(f,0x7f,sizeof(f));
	for(int i=1,x,y,z;i<=m;i++){
		scanf("%d%d%d",&x,&y,&z);//初始化鄰接矩陣
		if(f[x][y]<z) continue;
		f[x][y]=z;
		f[y][x]=z;
	}
	for(int i=1;i<=n;i++)
		f[i][i]=0;
	prim(1);//執行Prim算法
	printf("%d\n",total);
	return 0;
}

2020/8/15 13:37 update:增長了堆優化\(Prim\)的代碼

\(Code\) \(O(mlogn)\)

#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
#include<set>
#include<map>
#include<utility>
#include<iostream>
#include<list>
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<iomanip>
typedef long long int ll;
inline int read(){
	int fh=1,x=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
	return fh*x;
}
inline int _abs(const int x){ return x>=0?x:-x; }
inline int _max(const int x,const int y){ return x>=y?x:y; }
inline int _min(const int x,const int y){ return x<=y?x:y; }
inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
const int maxn=100005;
struct Edge{
	int cost,to;
};
//以上均爲缺省源,下面是主要部分
int n,m;//n個點m條邊的無向圖G
std::vector<Edge>v[maxn];//vector鄰接表建圖

//Prim算法 
std::priority_queue< std::pair<int,int> >Q; //小根堆優化,第一維存權值,第二維存點標號 
int vis[maxn]; 

inline int Prim(){
	int MST=0;
	vis[1]=1;
	for(unsigned int i=0;i<v[1].size();i++)//把1號點的全部連邊所有入堆
		Q.push(std::make_pair(-v[1][i].cost,v[1][i].to));//入堆時取反變成了小根堆 
	while(Q.size()!=0){
		while(vis[Q.top().second]){
			Q.pop();
			if(Q.size()==0) return MST;//若是這裏堆中沒有元素,下一次循環時會訪問無效內存,致使RE
                                                   //因此特判一下,若是堆中沒有元素,那麼說明已經完成了最小生成樹的求解,返回MST便可
		}	
		int x=Q.top().second;
		MST-=Q.top().first;//注意存邊的時候取反了,這裏再取反
		Q.pop();
		vis[x]=true;//把點x加入最小生成樹 
		for(unsigned int i=0;i<v[x].size();i++){ 
			int y=v[x][i].to,z=v[x][i].cost;
			if(!vis[y]) Q.push(std::make_pair(-z,y)); //若是y不在生成樹裏,纔會入堆
		}
	}
	return MST;
} 
int main(){
	n=read(),m=read();
	for(int i=0,x,y,z;i<m;i++){
		x=read(),y=read(),z=read();
		v[x].push_back((Edge){z,y});
		v[y].push_back((Edge){z,x});//建圖 
	}
	int ans=Prim();
	printf("%d\n",ans);
	return 0;
}

關於最小生成樹\(Prim\)算法的分享就到這裏,感謝您的閱讀!

相關文章
相關標籤/搜索