圖論之最小生成樹 Prim算法和Kruskal算法

1.最小生成樹問題

最小生成樹問題:一個圖有V個頂點,找到V-1條邊連接這V個頂點,使得總權值最小

最小生成樹問題通常是針對帶權無向圖、連通圖

如果圖不是連通圖:分別在每個連通分量上計算最小生成樹---最小生成森林

應用:電纜佈線設計、網絡設計、電路設計

最小生成樹爲我們提供讓所有點都連通,並且連通的點的權值最小的一個方案

2.切分定理

3.Prim算法

用於解決稠密圖的最小生成樹問題。

計算下圖的最小生成樹

算法流程:

首先將0結點爲一個切分,剩下的結點爲一個切分,那麼0-2、0-4、0-6、0-7就是橫切邊

根據切分定理,0-7的權值最小,那麼這時0-7這條邊一定是最小生成樹的一邊。將7也加入到切分中

橫切邊1-7的權值最小,將1加入切分中

橫切邊0-2的權值最小,將2加入切分中

橫切邊2-3的權值最小,將3加入切分中

橫切邊5-7的權值最小,將7加入切分中

1-3的權值最小,但是此時1-3已經不是橫切邊了,因爲1、3結點已經在切分中了

一直取最小的邊,如果不是橫切邊則扔掉,直到取到一個橫切邊

橫切邊4-5的權值最小,將4加入切分中

橫切邊2-6的權值最小,將6加入切分中

所有的節點已經遍歷完了,程序結束

Lazy Prim以邊爲空作爲結束條件,即:取到一條邊,如果不是橫切邊,則扔掉

直到不再有候選邊爲止

代碼實現:

1.數據定義

使用鄰接矩陣來表示圖

vector<Edge> mst數組存儲最小生成樹的邊 (邊包括起點,終點,邊的權值)
vector<bool> marked(n,false);//這個數組代表結點是否被加入切分中 

定義一個最小堆(使用優先隊列),存儲當前需要處理的邊,權值小的在隊首 
priority_queue<Edge,vector<Edge>,greater<Edge> > pq; 

這邊注意需要對Edge結構體的>運算符做重載

2.對單個結點操作的函數

要將某個節點加入到切分中,要做兩件事

  1.將該節點的marked設爲true,代表將該節點加入切分中

  2.遍歷這個節點的所有鄰邊,如果鄰邊對應的那個結點還不在切分中,說明是橫切邊,將這個邊加入到優先隊列中

3.主函數

1.首先對結點0進行操作,即marked[0]=true,將結點0的所有鄰邊加入優先隊列中

2.從隊列中不斷取出當前待選邊中,權值最小的邊(隊頭),判斷這條邊是否是橫切邊,如果是,將這條邊加入到mst數組中,並且對橫切邊未加入切分的那個點進行操作

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
//prim算法 
//使用鄰接矩陣來表示圖 
class DenseGraph{
public:
	int n;//n代表結點數
	vector<vector<double> > Graph;//鄰接矩陣 
	bool directed;//是否是有向圖 
	DenseGraph(int n,bool directed){
		this->n = n;
		this->directed = directed;
		for(int i=0; i<n; i++)
			Graph.push_back(vector<double>(n,0));
	} 
	//有權圖:給圖添加一條邊
	void addEdge(int v,int m,double w)
	{
		Graph[v][m] = w;
		if(!directed) //無向圖 
			Graph[m][v] = w;
	} 
	void print()
	{
		for(int i=0; i<n; i++)
		{
			for(int j=0; j<n; j++)
				cout<<Graph[i][j]<<" " ;
			cout<<endl;
		}
	}
	 
};
//最小生成樹的邊 
struct Edge{
	int v;
	int w;
	double weight;
	Edge(int v,int w, double weight){
		this->v =v ;
		this->w =w ;
		this->weight =weight ;
	}
	bool operator >(const Edge &n) const
	{
		return weight>n.weight;   //最小堆
	}	
}; 
//有權無向圖
int n=8; 
DenseGraph g2(n,false);
vector<Edge> mst;//最小生成樹的邊 
vector<bool> marked(n,false);//代表結點是否被加入切分中 
double mstWeight;//最小生成樹的權值 
//定義一個最小堆,存儲當前需要處理的邊,權值小的在對首 
priority_queue<Edge,vector<Edge>,greater<Edge> > pq; 

//訪問結點v
//1.將結點v加入切分 
//2.訪問結點v的所有鄰接邊,
//如果鄰接邊對應的那個點還沒被訪問過,
//說明是橫切邊,將這條邊加入到優先隊列中 
void visit(int v)
{
	marked[v] = true;
	for(int i=0; i<g2.n; i++)
	{
		//v-i存在邊,並且點i還沒有在切分中 
		if(g2.Graph[v][i]!=0 && !marked[i])
		{
			 Edge edge(v,i,g2.Graph[v][i]);
			 pq.push(edge);//將這條邊加入到優先隊列中 
		} 
	}
	
} 

int main()
{
	g2.addEdge(0,2,0.26);
	g2.addEdge(0,4,0.38);
	g2.addEdge(0,6,0.58);
	g2.addEdge(0,7,0.16);
	g2.addEdge(1,2,0.36);
	g2.addEdge(1,3,0.29);
	g2.addEdge(1,5,0.32);
	g2.addEdge(1,7,0.19);
	g2.addEdge(2,3,0.17);
	g2.addEdge(2,6,0.40);
	g2.addEdge(2,7,0.34);
	g2.addEdge(3,6,0.52);
	g2.addEdge(4,5,0.35);
	g2.addEdge(4,6,0.93);
	g2.addEdge(4,7,0.37);
	g2.addEdge(5,7,0.28);
	g2.print();
	
	//Prim算法
	visit(0);//先訪問結點0 
	
	while(!pq.empty())
	{
		Edge e = pq.top();//取出最小橫切邊
		pq.pop();
		//判斷這條邊是否是橫切邊 
		if(marked[e.v] == marked[e.w])
			continue; 
		
		mst.push_back(e);//將這條邊放入最小生成樹中 
		//哪個結點不在切分中就訪問哪個結點 
		if(!marked[e.v])
			visit(e.v);
		else  
			visit(e.w);
	} 
	//計算最小權值
	mstWeight=0.0;
	for(int i=0; i<mst.size(); i++)
		mstWeight+=mst[i].weight;
	
	cout<<"Prim Mst:"<<endl;
	for(int i=0; i<mst.size(); i++)
		cout<<mst[i].v<<"-"<<mst[i].w<<":"<<mst[i].weight<<endl; 
	cout<<"The Mst weight is:"<<mstWeight<<endl;
	return 0;
}

Prim的時間複雜度是O(ElogE)

4.Kruskal算法

算法流程:

每次都找最小邊,只要加入的邊不使得圖構成一個環就可以

1.將圖中所有的邊按照權值進行一次排序

2.每次取出還沒考慮的所有邊中的最短邊

1-3、1-5、2-7加入後會形成環,所以我們不考慮這條邊

1-2、4-7、0-4加入後會形成環,所以我們不考慮這條邊

已經有了V-1條邊,算法結束

 

怎麼判斷將一條邊加入到當前邊後會形成一個環?

使用並查集

比如想要將1-3加入進來

則判斷1的根節點和3的根節點是否相同,如果相同,說明1,3已經連接在一起了,如果我們再將1-3加入,就形成了環

 

代碼實現:

1.數據定義

跟Prim算法一樣

使用鄰接矩陣來表示圖

vector<Edge> mst數組存儲最小生成樹的邊 (邊包括起點,終點,邊的權值)
vector<bool> marked(n,false);//這個數組代表結點是否被加入切分中 

定義一個最小堆(使用優先隊列),存儲當前需要處理的邊,權值小的在隊首 
priority_queue<Edge,vector<Edge>,greater<Edge> > pq; 

這邊注意需要對Edge結構體的>運算符做重載

2.並查集定義

使用一個parent數組存儲,每個節點的父親節點

寫查找一個點的根節點的函數getParent,使用路徑壓縮優化

關於並查集,可以參考我的另外一篇博客:https://blog.csdn.net/qq_31965925/article/details/106265355

3.將所有的邊存入最小堆中

針對無向圖,存1-2 不存2-1,否則重複

4.從最小堆中取出當前的最短權值邊

判斷當前取出的邊兩個點是否連通,連通則不處理

不連通的話,將邊加入結果中,並且將兩個點使用並查集連接起來

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
//每個結點用一個結構體來表示
struct node{//新增結構體,表示與某個節點相連的節點號與權值
	int num;
	double weight;
}; 
//使用鄰接表來表示有權圖 
class SparseGraph{
public:
	int n;//n代表結點數
	vector<vector<node> > Graph;//鄰接表
	bool directed;//是否是有向圖 
	SparseGraph(int n,bool directed){
		this->n = n;
		this->directed = directed;
		for(int i=0; i<n; i++)
			Graph.push_back(vector<node>());
	} 
	//給圖添加一條邊
	void addEdge(int v,int w,double weight)
	{
		node node;
		node.num=w;
		node.weight = weight;
		Graph[v].push_back(node);
		if(!directed) //無向圖 
		{
			node.num=v;
			Graph[w].push_back(node);
		}
	} 
	 
	void print()
	{
		for(int i=0; i<n; i++)
		{
			cout<<i<<":";
			for(int j=0; j<Graph[i].size(); j++)
				cout<<Graph[i][j].num<<"("<<Graph[i][j].weight<<")"<<" " ;
			cout<<endl;
		}
	}
	 
};
//最小生成樹的邊 
struct Edge{
	int v;
	int w;
	double weight;
	Edge(int v,int w, double weight){
		this->v =v ;
		this->w =w ;
		this->weight =weight ;
	}
	bool operator >(const Edge &n) const
	{
		return weight>n.weight;   //最小堆
	}	
}; 
//有權有向圖
int n=8;  
SparseGraph g2(n,false);
vector<Edge> mst;//最小生成樹的邊 
double mstWeight;//最小生成樹的權值 
vector<int> parent(n,-1);//並查集
//並查集獲取根節點 
int getParent(int p)
{
	if(p!=parent[p])
		parent[p]=getParent(parent[p]) ;
	return parent[p];
}


int main()
{
	g2.addEdge(0,2,0.26);
	g2.addEdge(0,4,0.38);
	g2.addEdge(0,6,0.58);
	g2.addEdge(0,7,0.16);
	g2.addEdge(1,2,0.36);
	g2.addEdge(1,3,0.29);
	g2.addEdge(1,5,0.32);
	g2.addEdge(1,7,0.19);
	g2.addEdge(2,3,0.17);
	g2.addEdge(2,6,0.40);
	g2.addEdge(2,7,0.34);
	g2.addEdge(3,6,0.52);
	g2.addEdge(4,5,0.35);
	g2.addEdge(4,6,0.93);
	g2.addEdge(4,7,0.37);
	g2.addEdge(5,7,0.28);
	g2.print();
	
	//kruskal算法 
	//定義一個最小堆,存儲所有邊
	//針對無向圖,存1-2 不存2-1,否則重複 
	priority_queue<Edge,vector<Edge>,greater<Edge> > pq;
	// 遍歷每個結點的鄰接邊 ,將邊加入最小堆中 
	for(int i=0; i<n; i++)
	{
		for(int j=0; j<g2.Graph[i].size(); j++)
		{
			node v = g2.Graph[i][j];
			if(i<v.num)
			{
				Edge edge(i,v.num,v.weight);
				pq.push(edge);	
			}
		} 
	}
	cout<<pq.size()<<endl;
	for(int i=0; i<n; i++)
		parent[i]=i;
	while(!pq.empty() && mst.size()<n-1)
	{
		Edge edge = pq.top();
		pq.pop();
		//這條邊的兩個結點是相連的,會構成一個環,所以丟掉 
		if(getParent(edge.v)==getParent(edge.w))
			continue;
		mst.push_back(edge);
		//將這兩個節點加入同一集合中,連接起來
		int vRoot = getParent(edge.v);
		int wRoot = getParent(edge.w);
		parent[vRoot] = wRoot;//將一個結點的根節點連在另外一個結點下面 
	} 
	//計算最小權值
	mstWeight=0.0;
	for(int i=0; i<mst.size(); i++)
		mstWeight+=mst[i].weight;
	
	cout<<"Kruskal:"<<endl;
	for(int i=0; i<mst.size(); i++)
		cout<<mst[i].v<<"-"<<mst[i].w<<":"<<mst[i].weight<<endl; 
	cout<<"The Kruskal weight is:"<<mstWeight<<endl;
	return 0;
	
	
	
	
	return 0;
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章