一、前置知识
1.生成树的定义
⼀个连通图的⽣成树是⼀个极⼩的连通⼦图,它包含图中全部的n个顶点,但只有构成⼀棵树的n-1条边。
2.生成树的属性
(1)⼀个连通图可以有多个⽣成树;
(2)⼀个连通图的所有⽣成树都包含相同的顶点个数和边数;
(3)⽣成树当中不存在环;
(4)移除⽣成树中的任意⼀条边都会导致图的不连通, ⽣成树的边最少特性;
(5)在⽣成树中添加⼀条边会构成环。
(6)对于包含n个顶点的连通图,⽣成树包含n个顶点和n-1条边;
(7)对于包含n个顶点的⽆向完全图最多包含 n^n-2颗⽣成树。
3.最小生成树
所谓⼀个带权图的最⼩⽣成树,就是原图中边的权值最⼩的⽣成树 ,所谓最⼩是指边的权值之和⼩于或者等于其它⽣成树的边的权值之和。
上图中,原来的带权图可以⽣成左侧的两个最⼩⽣成树,这两颗最⼩⽣成树的权值之和最⼩,且包含原图中的所有顶点。
最小生成树:无向连通带权图
二、克鲁斯卡尔(Kruskal)算法
问题:给定一个无向连通带权图,该图含有n个顶点,e(e>=n-1) 条边,求该图的最小生成树:
选出(n-1)条边,使得该图仍然连通,并且这(n-1)条边的权值和最小
思路
1.选权值尽可能小的(n-1)条边-->排序:选择排序算法
先按照边权大小对边进行排序,选(n-1)次,每次选当前权值最小的边
2.选边时,选的每一条边都得是连接未连通的两个结点才行,不能成环。
如何判断是否成环:当前选的这条边是否连接了未连通的两个结点?
使用并查集实现:顶点的并查集
每一次选择的时候,都要找一下该边两个顶点的父亲,判断是否相同,若相同在同一个集合中,该边不能选;否则,该边可选,合并两个顶点所在的集合
时间复杂度:选择排序O(m^2)+并查集O(m)---->O(m^2) m为边数
kruskal时间复杂度取决于排序算法的时间复杂度
代码
#include <stdio.h>
#include <stdlib.h>
#define INF 65535
//邻接矩阵存图--带权无向图
int g[105][105];//假设最多有100个点
int nv,ne;
//边数组
typedef struct {
int u,v;//两个端点在邻接矩阵中的下标
int w;//权值
}Edge;
Edge e[5005];//边集数组存边,方便排序
void sorte(int l,int r)
{
int minn;
Edge tmp;
for(int i=l;i<r;i++)
{
minn=i;
for(int j=i+1;j<r;j++)
{
if(e[minn].w>e[j].w)
{
minn=j;
}
}
tmp=e[i];
e[i]=e[minn];
e[minn]=tmp;
}
printf("按权值升序排列:\n");
for(int i=0;i<r;i++)
{
printf("(%d %d) %d\n",e[i].u,e[i].v,e[i].w);
}
}
int find(int* f,int x)
{
if(x==f[x])
{
return x;
}
else{
return f[x]=find(f,f[x]);
}
}
void Kruskal()
{
int f[105];//并查集的父亲数组
for(int i=0;i<nv;i++)
{
f[i]=i;
}
printf("以下是组成最小生成树的边:\n");
for(int i=0;i<ne;i++)
{
int fu=find(f,e[i].u);
int fv=find(f,e[i].v);
if(fu!=fv)
{
f[fv]=fu;//合并
printf("(%d %d) %d\n",e[i].u,e[i].v,e[i].w);
}
}
}
int main()
{
scanf("%d %d",&nv,&ne);
//邻接矩阵初始化
for(int i=0;i<nv;i++)
for(int j=0;j<nv;j++)
{
if(i==j)
{
g[i][j]=g[j][i]=0;
}
else
{
g[i][j]=g[j][i]=INF;//无穷大
}
}
//s输入边,建图
int x,y,wi;
for(int i=0;i<ne;i++)
{
scanf("%d %d %d",&x,&y,&wi);
g[x][y]=g[y][x]=wi;
e[i].u=x;
e[i].v=y;
e[i].w=wi;
}
sorte(0,ne);
Kruskal();
return 0;
}
/*
测试用例:
9 15
0 1 3
0 5 4
1 6 6
6 5 7
1 2 8
1 8 5
2 8 2
2 3 12
8 3 11
6 3 14
6 7 9
5 4 18
3 7 6
7 4 1
3 4 10
*/
样例的图:
三、普⾥姆(Prim)算法
kruskal算法--->边
prim算法------->点
按照什么样的策略,把这n个点依次选到生成树中?
点分成两类:
(1)已经选到生成树中的点:已经构成一部分生成树了
(2)还未选到生成树中的点
初始时:所有的点属于(2)-->最终:所有的点属于(1)
定义点到生成树的最小距离 dist ,(1)中点的dist为0;(2)中点的dist为 与生成树中的若干个点直接相连,其中边的最小权值为dist
第一次:任选一个点
接下来的n-1次:选(2)中dist最小的点
思路
维护一个点到生成树的最小距离 dist[],初始化:把每个点的dist赋为无穷大;
维护一个标记数组vis[],vis[i]==0,i点没在生成树中,vis[i]==1,i点在生成树中
1.任选一个点做起点,把起点x加入到生成树中:vis[x]=1,dist[x]=0;
2.更新x的邻接点到生成树的最小距离
3.执行n-1次循环:
(1)找到vis==0,并且dist最小的点m
(2)把起点m加入到生成树中
(3)更新m的邻接点到生成树的最小距离
prim时间复杂度O(n^2)n为点数
代码
#include <stdio.h>
#include <stdlib.h>
#define INF 65535
//邻接矩阵存图--带权无向图
int g[105][105];//假设最多有100个点
int nv,ne;//点数,边数
int dist[105];
int vis[105];
int minn(int x,int y)
{
if(x>y) return y;
else return x;
}
void prim()
{
//初始化dist数组:
for(int i=0;i<nv;i++)
{
dist[i]=INF;
}
dist[0]=0;
vis[0]=1;
for(int i=1;i<nv;i++)
{
if(vis[i]==0)
{
dist[i]=minn(dist[i],g[i][0]);
}
}
int temp,t;
for(int i=1;i<nv;i++)//执行n-1次for循环,选n-1个点
{
temp=INF;//最小距离
t=-1;//点的下标
for(int j=0;j<nv;j++)
{
if(vis[j]==0&&dist[j]<temp)
{
temp=dist[j];
t=j;
}
}
if(t==-1)
{
break;
}
vis[t]=1;
printf("点:%d,通过权值为 %d 边加入到最小生成树中\n",t,temp);
for(int j=0;j<nv;j++)
{
if(vis[j]==0)
{
dist[j]=minn(dist[j],g[j][t]);
}
}
}
}
int main()
{
scanf("%d %d",&nv,&ne);
//邻接矩阵初始化
for(int i=0;i<nv;i++)
for(int j=0;j<nv;j++)
{
if(i==j)
{
g[i][j]=g[j][i]=0;
}
else
{
g[i][j]=g[j][i]=INF;
}
}
//s输入边,建图
int x,y,wi;
for(int i=0;i<ne;i++)
{
scanf("%d %d %d",&x,&y,&wi);
g[x][y]=g[y][x]=wi;
}
prim();
return 0;
}
/*
测试用例:同kruskal算法样例图
9 15
0 1 3
0 5 4
1 6 6
6 5 7
1 2 8
1 8 5
2 8 2
2 3 12
8 3 11
6 3 14
6 7 9
5 4 18
3 7 6
7 4 1
3 4 10
*/
四、kruskal算法和prim算法的时间复杂度的选择
kruskal时间复杂度:选择排序O(m^2)+并查集O(m)---->O(m^2) m为边数
prim时间复杂度O(n^2)n为点数
因此,稀疏图(边少点多)用kruskal;稠密图(边多点少)用prim