图论之最小生成树 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;
}

 

 

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