最小生成树作为图论的重要算法之一,难度不言而喻,那么只要把握这几种算法,就很容易完成啦。但要区分于单源最短路和多源最短路,最小生成树是没有初始结点的,也就是它只求连通图,可以从任意点开始连。
算法1:Kruskal
Kruskal算法本质类似于并查集,将所有点连通至一个集合完成整个图的连接,这里很重要的一点就是并查集的压缩路径,没有压缩过的时间复杂度会变得很高。并查集的压缩路径可以看一下我的另一篇博客,并查集的模板。这里强调一点并查集的初始化很重要,不要忘记
struct node
{
int x,y,weight;
}node[1000000]; //用来存储每条边的值和端点
int arr[1005];//并查集数组
void Union(int a,int b){}//连接结点
int find(int a){}//寻找头结点
最小生成树顾名思义我们需要找出最短值的边使整个集合连通,那么我们可以根据已给出的所有边的值进行排序。
bool cmp(struct node a,struct node b)
{
return a.weight<b.weight;
}
然后调用sort函数,sort(node,node+m,cmp),m在这里指的是边的数量。
接下来,我们将边从小到大进行连接,判断条件为他们是否在同一个集合内,因为一旦在同一个集合内就表明,在此边之前有一个比它更小的边完成了这两个点的连接,已经不需要了,这里的连接并不是直连,而是在同一集合内的连接。所以每一条边,我们判断它的端点是否已经在同一集合内,因为边的值会越变越大,我们只需要判断连接即可。那么降低时间复杂度的一点就是,如果这个图已经完成了连接,也就是所有点的顶点相同,那么就可以结束对边的循环。又或者说只需要n-1条边即可,因为对于n个顶点的连通图,我们仅需要n-1条边,那么一旦出现n-1条边,我们就可以结束循环。所以最好的情况,Kruskal的循环只需要n-1次,最坏的情况需要m次。但这里并没有包括并查集的复杂度,所以整体最好还是会达到O(n^2),最坏O(n*m);
接下来,Kruskal模板
int Kruskal()
{
int sum=0,num=0;
for(int i=0;i<m;i++)
{
if(find(node[i].x)!=find(node[i].y))//判断是否在同一集合内
{
Union(node[i].x,node[i].y);//连接两个端点
sum+=node[i].weight;//加上这条边的权值
num++;//连接边数+1
}
if(num==n-1) break;
}
return sum;
}
算法2:prim
相比如Kruskal算法,prim算法比较难懂,但是理解过后便会对最短路有了更清晰的认识,有利于向Dijkstra算法的延伸。Dijkstra算法
由于技术有限,只能通过文字讲解,所以直接上代码,结合代码理解最佳。
int pic[1005][1005];//存图
在输入之前,初始化图,将各点之间的距离设置为最大值,表示无法到达。
void init()
{
for(int i=0;i<1005;i++)
{
for(int j=0;j<1005;j++)
{
pic[i][j]=(1<<21);//最大值可以自己设定,只要能表示不可达即可
}
}
}
接着对图进行赋值,区别于Kruskal这里的值是双向的。
prim模板函数如下
void prim()
{
int weight[1005];//起始到其他端点的值
for(int i=0;i<n;i++)
{
weight[i]=pic[0][i];//将图中0到各个端点的值赋给weight数组
}
pic[0][0]=0;
weight[0]=0;//表明0到0之间的距离为0
ll sum=0;//用来求距离和
//此时起点为0
for(int i=1;i<n;i++)//0点作为起始点,从下一个点开始
{
int j=0,k=0,min=(1<<31)-1;
//weight内的值为0时表示,已经访问过
//遍历weight数组,寻找一个最小值,并且该点没有被访问过
while(j<n)
{
if(min>weight[j]&&weight[j]!=0)
{
min=weight[j];
k=j;//用k存下这个节点
}
j++;
}
//已知的min是我当前起始节点到k节点的距离,已知连接边中最短的
sum+=min;
weight[k]=0;//访问过该节点
//每一个k的值就是连接顺序的点。
//自此形成从起始到K的边,将其连接
//更新weight数组,一旦新点的连通其他点的值小于原来节点连通值时,更新
for(int m=1;m<n;m++)
{
if(weight[m]>pic[k][m]&&weight[m]!=0)
{
weight[m]=pic[k][m];
}
}
}
}
weight数组是最难理解的一个部分,它实际上表明的是当前这一集合到剩余点的距离,举个例子,0是起始点,短-表示连接,0-1=2,0-2=3,0-3=4,weight[1]=2,weight[2]=3,weight[3]=4。那么我会取1进行连接。接下来1-2=1,1-3=3,此时比较与0-2,我会发现1-2更短,而0和1其实已经是一个整体,那么这个整体到2的连接最短无疑是1-2,于是将weight[2]内的值替换为1,weight[3]同理。时间复杂度达到了O(n^2)。
算法3:Sollin
这个算法同样适用于最小生成树,类似于prim和并查集的集合体,相比于前两种主流的算法,优势并不大,所以不过多介绍了。Sollin模板,并查集部分自行补全。
struct node
{
int u;
int v;
int w;
}edge[105];
int d[105];//各子树的最小连外边的权值
int e[105];//各子树的最小连外边的索引
bool visit[105];//防止边重复统计
int fa[105];
int Boruvka()
{
int tot=0;
for(int i=1;i<=n;i++) fa[i]=i;
while(true)
{
int cur=0;
for(int i=0;i<n;i++) d[i]=(1<<21);
for(int i=0;i<m;i++)
{
int a=find(edge[i].u),b=find(edge[i].v),c=edge[i].w;
if(a==b) continue;
cur++;
if(c<d[a]||c==d[a]&&i<e[a]) d[a]=c,e[a]=i;
if(c<d[b]||c==d[b]&&i<e[b]) d[b]=c,e[b]=i;
}
if(cur==0) break;
for(int i=0;i<n;i++)
{
if(d[i]!=inf&&!visit[e[i]])
{
join(edge[e[i]].u,edge[e[i]].v);
tot+=edge[e[i]].w;
visit[e[i]]=true;
}
}
}
return tot;
}