最小生成树

12 篇文章 0 订阅

1.Kruskal算法

首先按照变得权值从小到大排序,每次从剩余的边中选出权值最小的且两个顶点不在同一集合中的边(为的是不产生回路),加入到生成树中,直到加入了n-1条边为止

对边进行快排O(MkogM),在m条边中选出n-1条边是O(MlogN),所以Kruskal算法的时间复杂度为O(MlogM +MlogN),通常M要比N大得多,因此最终算法的时间复杂度为

O(MlogM)

Kruskal算法是一步步将森林中的树合并

利用Kruskal算法可以求一个图的最大生成树和最小生成树(如果存在的话),不存在最下(最大)生成树的条件是并查集里面并没有n个点,也就是cnt<n-1


#include<iostream>    
#include<cstdio>    
#include<cstdlib>    
#include<cstring>    
#include<string>    
#include<queue>    
#include<algorithm>    
#include<map>    
#include<iomanip>    
#define INF 99999999    
#define maxn 20   
using namespace std;    
    
int f[maxn];//存放的i点的祖先 
int n,m;
struct node{
	int x,y;
	int w;
}; 
node edge[maxn];//存储边的数组 

int cmp(node a, node b)//比较函数,用来快排用,从小到大排序 
{
	return a.w<b.w;
}
void init()//每个点的祖先都是他自己,其实就是每个点之间都没有关系 
{
	for(int i=1;i<=n;i++)
		f[i]=i;
}

int getf(int i)//获得该点的祖先 
{
	int temp;
	if(f[i]==i)
		return i;
	else
	{
		f[i]=getf(f[i]);
		return f[i];
	}
}

int merge(int a, int b)//将两个点合并到一棵树里,其实就是把a到b这条边放进生成树里 
{
	int i=getf(a);
	int j=getf(b);
	if(j==i)
		return 0;
	else
	{
		f[j]=i;
		return 1;
	}
}

int main()
{
	int sum=0,cnt=0;
	scanf("%d%d",&n,&m);
	for(int i=0;i<m;i++)
	{
		scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].w);
	}
	sort(edge,edge+m,cmp);//进行边的权值的从小到大排序 
	init();//初始化每个点 
	//Kruskal算法的核心部分 
	for(int i=0;i<m;i++)
	{
		if(cnt<=n-1)//当生成了n-1边,结束,最小生成树只有n-1条边 
		{
			if(merge(edge[i].x,edge[i].y))//如果两个点不在一个集合里,也就是说他俩的祖先不是同一个,就把这条边加入到生成树里面 
			{
				sum+=edge[i].w;//sum存放的是最小生成树的所有边的权值之和 
				cnt++;
			}
		}
		else
			break;
	}
	
	printf("%d\n",sum);
	return 0;
}
/*
输入数据 
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

输出结果
 19
*/



2.Prime 算法

(1)邻接表+堆排序+Dijkstra算法   时间复杂度为O(MlogN),次算法适用于稀疏图

Prime算法是通过每增加一条边来建立一棵树

Dijkstra算法是用来求一个图的最小生成树,但是无法求出一个图的最大生成树

该算法的一个关键地方理解dis数组的含义,dis数组里面存放的是每个点到这棵树的距离,最基本的Dijkstra算法中dis存放的是每个点到1号(假定求的是1号点到其他点的最短距离)点的距离吗,一定要把这个地方理解好

#include<iostream>
#include<cstring>
#include<string>    
#include<queue>    
#include<algorithm>    
#include<map>    
#include<iomanip>    
#define inf 0x3f3f3f3f    
#define maxn 20   
using namespace std;  
int n,m,sum;  
int heap[maxn],pos[maxn];//pos数组是一个重要部分,用来记录dis中对应的编号在heap中的对应位置 
int head[maxn],book[maxn];
int dis[maxn];
struct node{
	int front;
	int to;
	int w;
	int next;
};
node edge[maxn];

void swap(int i, int j)
{
	//交换堆中i,j位置中的值 
	int t;
	t=heap[i];
	heap[i]=heap[j];
	heap[j]=t;
	//改变pos数组
	pos[heap[i]]=i;
	pos[heap[j]]=j; 
	return;
}

void siftdown(int i)
{
	int t,flag=1;
	while(i*2<=n && flag)
	{
		if(dis[heap[i*2]]<dis[heap[i]])
			t=i*2;
		else t=i;
		
		if(i*2+1<=n)
		{
			if(dis[heap[i*2+1]]<dis[heap[t]])
				t=i*2+1;
		}
		
		if(i!=t)
		{
			swap(i,t);
			i=t;
		}
		else flag=0;
	}
	return;
}
void siftup(int i)
{
	int t,flag=1;
	while(i!=1 && flag)//当i=1时,就没法再继续向上寻找了 
	{
		if(dis[heap[i/2]]<dis[heap[i]])//父节点比子节点小,符合要求,不需要交换位置 
			flag=0;
		else swap(i,i/2);
		i=i/2;//这句很重要一定要有,交换子节点和父节点的位置后,i更新为父节点位置,方便下次继续向上探索 
		
	}
	return;
}

void creat()//创建堆 
{
	for(int i=n/2;i>=1;i--)
		siftdown(i);
}
int pop()//弹出堆中的第一个元素 
{
	int t;
	t=heap[1];
	heap[1]=heap[n];
	pos[heap[n]]=1;
	n--;//记得n一定要减1 
	siftdown(1);
	return t;
} 
int main()
{
	int num;
	scanf("%d%d",&n,&m);
	num=n;
	for(int i=0;i<m;i++)
		scanf("%d%d%d",&edge[i].front,&edge[i].to,&edge[i].w);
	memset(head,-1,sizeof(head));
	for(int i=m;i<2*m;i++)//无向图 ,做题的时候一方要看清是无向图还是有向图,因为图的存储方式会不一样 
	{
		edge[i].front=edge[i-m].to;//这个地方一定要看清楚front与to相反的,与小于m相比 
		edge[i].to=edge[i-m].front;
		edge[i].w=edge[i-m].w;
	}	
	//邻接表的建立
	for(int i=0;i<2*m;i++)
	{
		edge[i].next=head[edge[i].front];
		head[edge[i].front]=i;
	} 
	//堆的初始化 
	for(int i=1;i<=n;i++)
	{
		heap[i]=i;//存放的是dis数组的编号,是按照dis数组编号里面存储的数值的大小进行堆排序的 
		pos[i]=i;//存放的是dis数组编号在heap中的位置 
	}
	//dis数组的初始化 
	dis[1]=0;
	for(int i=2;i<=n;i++)//dis一定要全部初始化为inf 
		dis[i]=inf;
	int k=head[1];
	while(k!=-1)
	{
		dis[edge[k].to]=edge[k].w;
		k=edge[k].next;
	}
	book[1]=1;
	creat();//堆的创建 
	pop();
	//单源最短路径--Dijkstra算法核心部分 
	while(n>0)
	{	
		int t;
		t=pop();//弹出的是heap[1]的值,即dis中最小的编号 
		        //每弹出一个值,就相当于堆的大小要减1 
		book[t]=1;
		sum+=dis[t];
		k=head[t];
		while(k!=-1)//松弛 
		{
			if(book[edge[k].to]==0 && dis[edge[k].to]>edge[k].w) //松弛的每个点到这棵生成树的距离
			{
				dis[edge[k].to]=edge[k].w;
				siftup(pos[edge[k].to]);//因为有堆的存在,所以每次改变dis数组里面的值都要相当于对dis输入进行一次重新排序, 
			}                           //确保heap数组第一个元素的值是最小的,所以要想上调整 
			k=edge[k].next;
		}
	}
	
	printf("%d\n",sum);
	return 0; 
	
}
/*
输入数据
 6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

输出结果
19 
*/ 


(2)没有使用堆的Prime算法

该算法的时间复杂度为O(N*N),该算法适用于稠密图,在稠密图里面适用该算法会比Kruskal算法快特别多,稠密图不适用于Prime算法+堆,因为这样的话时间复杂度会变成

N*Nlong(N),时间复杂度大于O(N*N)

#include<iostream>
#include<cstring>
#include<string>    
#include<queue>    
#include<algorithm>    
#include<map>    
#include<iomanip>    
#define inf 0x3f3f3f3f    
#define maxn 20   
using namespace std;  
int n,m,sum;  
int edge[maxn][maxn],book[maxn];//稠密图用邻接矩阵来存储就行了 
int  dis[maxn]; 

int main()
{
	int x,y,w,cnt=0;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(i==j)
				edge[i][j]=0;
			else edge[i][j]=inf;
	//邻接矩阵建立
	for(int i=0;i<m;i++) 
	{
		scanf("%d%d%d",&x,&y,&w);
		edge[x][y]=w;
		edge[y][x]=w;//无向图,注意要再反向存储一遍 
	}
	//dis数组初始化,第一个点选择1号顶点,因为每一个点都要选到,所以先选哪一个点无所谓 
	for(int i=1;i<=n;i++)
		dis[i]=edge[1][i];
	book[1]=1;
	cnt++;
	//Djikstra算法的核心部分  
	while(cnt<n)//这里一开始我写的是cnt<=n,结果错误,因为当cnt==n时,所有的点都已经选完了,再往下算就多加边了,自然出错,所以cnt<n,其实此时的cnt已经等于n了, 
	{           //也就是选完所有的点了,跳出while循环 
		int t,Min=inf;
		//找出dis数组中最小的边 
		for(int i=1;i<=n;i++)
		{
			if(book[i]==0 && dis[i]<Min)
			{
				Min=dis[i];
				t=i;
			}
		}
		
		book[t]=1;
		sum+=dis[t];
		printf("%d\n",dis[t]); 
		//松弛 
		for(int i=1;i<=n;i++)
		{
			if(book[i]==0 && edge[t][i]<dis[i]) //松弛的是每个点到这棵生成树的距离

				dis[i]=edge[t][i];
		}
		cnt++;
	}
	printf("%d\n",sum);
	return 0; 
	
}
/*
输入数据
 6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

输出结果
19 
*/ 



3.总结

因为kruskal算法是每次选最短的边,所以时间复杂的与边的数量有关,所以Kruskal算法适用于稀疏图(因为边的数量少),Prime算法每次选的是点,对边的依赖比较小,所以稠密图要用Prime算法,这个方法仅用来记忆用吗,具体算法的时间复杂度还需要进一步计算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值