一、概念
生成树:一个连通图的生成树,指的是该图的一个子图,它包含图的所有顶点(N个),但只有足够把所有顶点连接在一起的N-1条边。 如果再向其中添加一条边,那么必定会有环形成。
最小生成树:一个连通图的所有生成树中,所有边的权值加起来最小的生成树,称为最小生成树。
二、实际应用
学一种结构或者一种算法,我时常会考虑它跟实际生活中的哪些方面有联系, 是否可以运用到实际生活呢。而不只是知道可以作为题目的解法。
最小生成树在实际中可以用于通信网络铺设的计算,城市间道路的计算,我所查到的资料一般都是考虑费用为主。即运用最小生成树知识,考虑如何建设才能最节省费用并且可以投入实用。
三、两种算法
- Kruskal(克鲁斯卡尔)算法
学习参考博客(https://blog.csdn.net/luomingjun12315/article/details/47700237)
思想: - 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点u ,v 。
u ,v ,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。 - 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
用自己的话说就是:
先从小到大排序各条边的长度;
再依次搜索已排序好的边;
搜查到的边的两端点如果不在一个集合里,就合并两个集合;
亦即是两端点没在同一个连通图中,即合并两个点所在的图;
直到所有点在同一个集合(同一张图),即可。
因为每一次选取的都是能够和存放最小生成树点的集合中的点相连的最小的边,所以最后得到的边权总和最小。
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 1e6+5;
struct edge
{
//u v 为边的前后节点
int u;
int v;
int w;//权重
} e[MAXN];
int n,m;
int pre[MAXN],r[MAXN];
bool comp(const int a,const int b)
{
//间接比较,实现对边权大小的排序
return e[a].w<e[b].w;
}
int find(int x)
{
int t = x;
while(t!=pre[t])
t=pre[t]; //找到根结点
int i=x,j;
//优化步骤,让所有子节点都直接与父节点相连,使每次搜索父节点的效率提高很多
if(pre[i]!=t)//
while(t!=i)//压缩路径,将树的深度变成只有 2,即所有叶都直接连在同一根上
{
j=pre[i];
pre[i]=t;
i=j;
}
return t;
}
int Kruscal()
{
int ans=0;
for(int i=1;i<=n;++i)
pre[i] = i; //每个点的父节点一开始都是它本身,因为此时每个点占了一个集合
for(int i=1;i<=m;++i) // r[]数组存放的是边的序号
sort(r,r+m,comp); //对边的权值从小到大进行排序
for(int i=1; i<=m; ++i)
{
int x=find(e[r[i]].u);
int y=find(e[r[i]].v);
if(x!=y)//两个点不在同一个集合,则合并
{
//根节点相连
pre[x]=y; //将其中一个设置为另外一个的父节点,则连接了两个集合
ans+=e[r[i]].w;
}
}
return ans; //最小总权值
}
int main()
{
cin>>n>>m;
for(int i=1; i<=m; ++i)
{ // cout<<"请输入第"<<i<<"条边:";
cin>>e[i].u>>e[i].v>>e[i].w;
r[i] = i;
}
//排序各边
sort(r,r+m,comp);
//每个点生成独立的集合
for(int i=1; i<=n; ++i)
pre[i]=i;
cout<<Kruscal()<<endl;
}
其中使用了并查集的思想。
(学习参考博客:https://blog.csdn.net/luomingjun12315/article/details/47373345)
并查集 :是一种用于管理分组的数据结构,它可以查询a和b两个元素是否为同一个集合的元素,如果不是且有需要,则把a和b并为同一个集合。
最小生成树中,可以把每个连通分量看成一个集合,该集合包含了连通分量中的所有点。这些点互相连通,没有前后顺序之分,如同一个集合中的元素一样,只有“属于”和“不属于”的区别。
在上面代码的 find 函数中 使用了一个循环对并查集做了优化,使得子节点都直接跟父节点相连,大大提高了查找效率。
2.Prime(普里姆)算法
学习参考博客(https://blog.csdn.net/lqcsp/article/details/14118871)
思想:在带权连通图中V是包含所有顶点的集合, U已经在最小生成树中的节点的集合,从图中任意某一顶点v开始,此时集合U={v},重复执行下述操作:在所有u∈U,v∈V-U的边(u,v)∈E中找到一条权值最小的边,将(u,w)这条边加入到已找到边的集合,并且将点w加入到集合U中,当U=V时,就找到了这颗最小生成树。
个人觉得跟Kruskal算法类似。Kruskal算法偏重于边,对所有边排序后遍历边,最坏的情况可能遍历到最后,在稀疏图使用效率不错,但稠密图使用效率或许就不太高了。而Prime算法偏重于点,根据点去找边,在稠密图使用时相比于Kruskal算法来说好一点。
//任意选择一个起点 u
//每次把点添加到最小生成树点的集合U中,都把添加进去的点跟集合内其他点的距离设置为0,表示在集合中
int Prime(int u)
{
for (int i = 1; i <= n; i++)
dis[i] = (i==u?0:g[u][i]); //dis[i] 表示是的 节点 i 到集合U的最短距离
//N-1次遍历,每次都从集合N中找到一个结点,距离集合Y中所有结点值最小
for (int i = 1; i < n; i++)
{
int temp=INF,k;
for (int j = 0; j < n; j++)
//找一条到集合U的最小的边
if(dis[j]&&dis[j]<temp )
{
temp = dis[j];
k = j;
}
ans+=temp;
dis[k] = 0;//将节点加入到 U(最小生成树点集)中
//更新N集合中剩余的其他结点,因为dis[k]发生了变化
for (int j=0; j<n; j++)
if(dis[j] > g[j][k])
dis[j] = g[j][k];
}
return ans;//最小总权值
}
先学这两种基本的比较常用的,一些优化方法以后再深入学习。
如果读者对上面的代码有优化的方法,请多多指教。