克鲁斯卡尔求最小生成树——并查集的应用

参考:https://www.cnblogs.com/yoke/p/6697013.html

克鲁斯卡尔算法

克鲁斯卡尔算法的基本思想是以边为主导地位,始终选择当前可用(所选的边不能构成回路)的最小权植边。所以Kruskal算法的第一步是给所有的边按照从小到大的顺序排序。这一步可以直接使用库函数qsort或者sort。接下来从小到大依次考察每一条边(u,v)。
具体实现过程如下:
<1> 设一个有n个顶点的连通网络为G(V,E),最初先构造一个只有n个顶点,没有边的非连通图T={V,空},图中每个顶点自成一格连通分量。
<2> 在E中选择一条具有最小权植的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则,即这条边的两个顶点落到同一连通分量 上,则将此边舍去(此后永不选用这条边),重新选择一条权植最小的边。
<3> 如此重复下去,直到所有顶点在同一连通分量上为止。
下面是伪代码:

 1 // 把所有边排序,记第i小的边为e[i] (1<=i<=m)m为边的个数 
 2 // 初始化MST为空
 3 // 初始化连通分量,使每个点各自成为一个独立的连通分量
 4 
 5 for (int i = 0; i < m; i++)
 6 {
 7     if (e[i].u和e[i].v不在同一连通分量)
 8     {
 9         // 把边e[i]加入MST
10         // 合并e[i].u和e[i].v所在的连通分量 
11     } 
12 }

上面的伪代码,最关键的地方在于“连通分量的查询和合并”,需要知道任意两个点是否在同一连通分量中,还需要合并两个连通分量。
这个问题正好可以用并查集完美的解决

并查集

并查集(Union-Find set)这个数据结构可以方便快速的解决这个问题。基本的处理思想是:初始时把每个对象看作是一个单元素集合;然后依次按顺序读***通边,将连通边中的两个元素合并。在此过程中将重复使用一个搜索(Find)运算,确定一个集合在那个集合中。当读入一个连通边(u,v)时,先判断u和v是否在同一个集合中,如果是则不用合并;如果不是,则用一个合并(Union)运算把u、v所在集合合并,使得这两个集合中的任意两个元素都连通。因此并查集在处理时,主要用到搜索和合并两个运算。
为了方便并查集的描述与实现,通常把先后加入到一个集合中的元素表示成一个树结构,并用根结点的序号来表示这个集合。因此定义一个parent[n]的数组,parent[i]中存放的就是结点i所在的树中结点i的父亲节点的序号。例如,如果parent[4]=5,就是说4号结点的父亲结点是5号结点。约定:如果i的父结点(即parent[i])是负数,则表示结点i就是它所在的集合的根结点,因为集合中没有结点的序号是负的;并且用负数的绝对值作为这个集合中所含结点的个数。例如,如果parent[7]=-4,说明7号结点就是它所在集合的根结点,这个集合有四个元素。初始时结点的parent值为-1(每个结点都是根结点,只包含它自己一个元素)。

代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//边类
class edge
{
   
public:
 int i, j, w;
 edge(int i, int j, int w) :i(i), j(j), w(w) {
   }
};
vector<edge> ev;
int n, * parent,**a;
//sort的比较函数
bool cmp(edge a, edge b)
{
   
 return a.w < b.w;
}
//初始化parent数组
void initparent()
{
   
 for (int i = 0; i < n; ++i)
  parent[i] = -1;
}
//查找树根
int find(int x)
{
   
 int s;
 for (s = x; parent[s] >= 0; s = parent[s]);
 while (s != x)
 {
   
  int temp = parent[x];
  parent[x] = s;
  x = temp;
 }
 return s;
}
//合并
void Union(int a,int b)
{
   
 int p1, p2,temp;
 p1 = find(a);
 p2 = find(b);
 temp = parent[p1] + parent[p2];
 if (parent[a] > parent[b])//此处注意都是负数
 {
   
  parent[p1] = p2;
  parent[p2] = temp;
 }
 else
 {
   
  parent[p2] = p1;
  parent[p1] = temp;
 }
}
//克鲁斯卡尔算法
void Kruskal()
{
   
 int sumw = 0, nume = 0, u,v;
 initparent();
 for (auto k : ev)
 {
   
  u = k.i; v = k.j;
  if (find(u) != find(v))
  {
   
   sumw += k.w;
   Union(u, v);
   a[u][v] = k.w;
   a[v][u] = k.w;
  }
  if (nume >= n - 1)break;
 }
 cout << "最小生成树的权值为" << sumw << endl;
}
int main()
{
   
cout << "输入矩阵维数:" << endl;
 cin >> n;
 a = new int* [n];
 for (int i = 0; i < n; ++i)
  a[i] = new int[n];
  parent = new int[n];
 cout << "输入赋权图" << endl;
 for(int i=0;i<n;++i)
  for (int j = 0; j < n; ++j)
  {
   
   a[i][j] = 0;
   int w;
   cin >> w;
   if(w!=0)
   ev.push_back({
    i, j, w });
  }
 sort(ev.begin(), ev.end(), cmp);
 Kruskal();
 cout << "最小生成树:" << endl << '\t';
 for (int i = 0; i < n; ++i)
  cout << i << '\t';
 cout << endl;
 for (int i = 0; i < n; ++i)
 {
   
  cout << i << '\t';
  for (int j = 0; j < n; ++j)
   cout << a[i][j] << '\t';
  cout << endl;
 }
}

实验的分析与思考

该实验利用克鲁斯卡尔算法求最小生成树,特点为利用了并查集的相关知识。克鲁斯卡尔算法中的两个重要操作:
1.判断当前边的两个节点是否位于两个不同的连通分量中。
2.若分别位于两个不同的连通分量则把这两个连通分量合并。
这两个操作并查集能够完美的执行。分别利用find函数和union函数即可。
解决了这两个问题,按照伪代码就能够直接写出。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务