问题分析
首先复习以下最小生成树的相关概念:
生成树:一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。
最小生成树:当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。
Kruskal算法是求解图中最小生成树的一个经典算法。算法思路如下:
1、寻找图中权重最小的边加入到最小生成树中,并将该边从图中删去;
2、若加入该边后生成树中构成了回路,则放弃该边,回到步骤1;
3、若图中所有边都浏览过或者最小生成树中已有(n-1)条边,则算法结束,否则回到步骤1。
Kruskal算法符合贪心算法的思想,每次都是选取当前图中权重最小的边进行比较选择,是比较好理解的。下面看一下简单的示例:
假设提供的图如下:
共有a,b,c,d四个顶点以及四条边,权重分别为1,2,5,6;
选取权重为1的边,无回路,保存该边;
选取权重为2的边,无回路,保存该边;
选取权重为5的边,出现回路,舍弃该边;
选取权重为6的边,不存在回路,保存该边;
图中每条边都浏览完毕,算法结束,最后得到的树就是最小生成树。
算法伪代码
struct Node
{
int left;//边的一个顶点
int right;//边的另一个顶点
int weight;//权重
friend bool operator<(const Node s1, const Node s2)
{
return s1.weight > s2.weight;//按照权重在优先队列中从小到大排列
}
};
int n;//记录顶点数
int num=0;//记录最小生成树中边的个数
priority_queue open;//记录图中的各边信息
queue close;//记录保存在最小生成树中的各边信息
void Kruskal()
{
while (open表中不为空)//浏览图中每一个边后停止
{
Node temp = open.top();//为浏览过的图的边中权重最小的边
open.pop();
if (现在选取的边加入到close中后使得“最小生成树”出现回路)
舍弃该边,直接进入到下一次循环;
将该边的信息放入到close表中;
num++;
if(num==n-1)
最小生成树已经找到,退出while循环;
}
//最终close表中的信息就是构成最小生成树各边的信息
}
并查集的实现
在寻找最小生成树的过程中,每选择加入一条边,需要判断是否构成回路,如果构成回路,就去掉该边重新选取。我们可以直观的看出是否构成回路,但是代码又该如何实现呢?这与树和图的性质有关,针对本题我们引出并查集的相关知识。
定义:并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
主要构成:并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。
并查集原本的作用是用来计算图中有几个连通分量的数据结构,但是本题我们用的并不是这个功能,而是判断是否构成回路,所以下面举个例子来看一下如何利用并查集判断图中是否构成回路。
蓝色边代表还未插入到图中,下列表格记录的pre[ ]数组的信息。
i 1 2 3 4 5 6 7
pre[ i ] 1 1 1 2 2 3 7
pre[ ] 记录了每个点的前驱节点是谁,1节点为老大,没有前驱节点,所以pre[ 1 ] = 1,7节点此时也是如此,2和3节点的前驱节点都是1,4和5的前驱节点都是2,6的前驱节点是3。
现在我们尝试往图中加入边first。first的相关节点是6和7,我们通过一层层判断6和7的最上级是谁,该过程由find(x)函数实现,两个节点最上级如果相同,那边加入此边后,图就会含有回路,反之没有回路。
证明如下:
如果find(a)=find(b),那么我们可以判断a节点和b节点是可达的,那么这时候比存在若干节点v,使得有条路连通a和b,这时我们再将ab边加入其中,该路就变成了回路。
回到我们的示例中,find(6)=1,find(7)=7,两者不等,我们就可以把边first加入到图中并且不会产生回路。
find()函数实现如下:
int find(int x)
{
if (pre[x] == x)
return x;
else
return pre[x] = find(pre[x]);
}
将边first加入到图中,实际上也就是两个连通分量的合并,意味着我们要对pre[ ]数组进行更新,将其中一个连通分量的老大设为另一个连通分量的小弟即可,至于到底谁是老大无所谓(这一点跟画出来的图或树会有所冲突,但是没有关系,我们能达到判断是否构成回路的目的即可),join()函数实现代码如下:
void join(int x, int y)
{
pre[find(x)] = find(y);
}
更新后的pre[ ]数组如下
i 1 2 3 4 5 6 7
pre[ i ] 7 1 1 2 2 3 7
再将second边放入到图中,可以通过find()函数得到find(5)=7,find(6)=7,所以不能将second边放入到图中。
以上就是对于并查集在本题中的相关应用了,在最开始别忘了对于pre[ ]数组进行初始化,此时一个节点就是一个连通分量,每个节点都是各自分量的老大,所以将设置pre[ i ] = i 就可以了。
完整代码
#include <iostream>
#include <queue>
using namespace std;
#define N 150
struct Node
{
int left;
int right;
int weight;
friend bool operator<(const Node s1, const Node s2)
{
return s1.weight > s2.weight;
}
};
int n;
int num;
int M[N][N];
priority_queue<Node> open;
queue<Node> close;
int pre[N]; // 并查集数组,表示每个节点的父亲节点
void Init()
{
int i, j;
for (i = 1; i <= n; i++)
for (j = i + 1; j <= n; j++)
{
if (M[i][j] == 0)
continue;
Node temp = {i, j, M[i][j]};
open.push(temp);
}
for (int i = 1; i <= n; i++)
pre[i] = i;
}
// 查找x所在连通分量的根节点
int find(int x)
{
if (pre[x] == x)
return x;
else
return pre[x] = find(pre[x]);
}
// 合并x和y所在的连通分量
void join(int x, int y)
{
pre[find(x)] = find(y);
}
// 判断边(u,v)是否构成环
bool hasCircle(int u, int v)
{
if (find(u) == find(v))
return true;
else
{
join(u, v);
return false;
}
}
void Kruskal()
{
while (!open.empty())
{
Node temp = open.top();
open.pop();
if (hasCircle(temp.left, temp.right))
continue;
++num;
close.push(temp);
if(num==n-1)
break;
}
}
void Print()
{
while (!close.empty())
{
Node temp = close.front();
close.pop();
cout << temp.left << ' ' << temp.right << ' ' << temp.weight;
cout << endl;
}
}
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
cin >> M[i][j];
Init();
Kruskal();
Print();
system("Pause");
return 0;
}