首先按照变得权值从小到大排序,每次从剩余的边中选出权值最小的且两个顶点不在同一集合中的边(为的是不产生回路),加入到生成树中,直到加入了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算法,这个方法仅用来记忆用吗,具体算法的时间复杂度还需要进一步计算