在图论领域,最小生成树是一种较为常见的树形结构。一个有 n 个结点的某连通图的生成树是原图的较小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通性的最少的边。最小生成树可以使用Kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。在本篇文章中,将会对这Kruskal算法进行介绍。
并查集
在介绍Kruskal算法之前,需要掌握“并查集”。
在一些有n个元素的集合应用问题中,我们常常是在开始时让每个元素构成单元素的集合,然后按一定顺序将属于同一组的元素所在的集合进行合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
简单来说,这是一种查询效率极高的算法,通过并查集的使用,可以极大减少重复查询的时间。对建立最小生成树有着重要的作用。
并查集是整型数组
并查集并不是使用一个set,而是建立一个整型数组,通过下表及其对应的值建立的所属关系。我的习惯是使用father[N]表示。这里我尽量减少概念性的内容,主要以算法的代码实现为主。理论部分可以参考
#define N 100005
int father[N];
其中N表示节点的最大数量。
void init()
{
for(int i = 0; i < n; i++)
father[i] = i;
}
这个函数在每次进行对应操作前都要调用,进行father的初始化。初始化的时候,每一个节点 i 的集合都是数值和自己一样,表示自己单独是一个集合,没有进行任何合并。
int findx(int x)
{
if(father[x] == x)
return x;
return findx(father[x]);
}
这个函数的作用是查询编号为x的节点对应在哪个编号的集合里面。if条件那里与下面介绍的“合并”操作有关。简单来说,每一个集合里面一定有一个点的编号和集合的编号一致,只要我们找到当前x和哪个点是一个集合,就能确定x在哪个集合。下面将有例子说明。
void merge(int x, int y)
{
int a = findx(x);
int b = findx(y);
if(a == b)
return;
father[a] = b;
}
这个函数的作用是合并两个点(集合)。由于findx将x和y对应的 满足father[x] == x 的点找出来了,所以将x所在集合的“中心点”a移动到b中,这样在后续的查找时,(father[a] != a)而是(father[a] == b ),实现了两个集合的“合并”。
至此,并查集的三大方式就介绍完毕。下面看一个例子。
假设有5个点。
x | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
father[x] | 0 | 1 | 2 | 3 | 4 |
现在合并1和2,merge(1,2),并查集变为
x | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
father[x] | 0 | 2 | 2 | 3 | 4 |
现在合并1和3,merge(1,3),并查集变为
x | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
father[x] | 0 | 2 | 3 | 3 | 4 |
这样,在查找1的时候,就会递归查找到3,查找2的时候,也会递归查找到3。
在搞清楚并查集的查找过程之后,可以进行以下改进
int findx(int x)
{
return father[x] == x ? x : father[x] = findx(father[x]);
}
整体框架是?:语句,当不一致的时候,同样会递归调用,主要区别在于:在每一次递归调用完毕,都会将中间查找结果赋值给对应father[x],这样简化了后续递归调用次数。(还是需要仔细想一想的)
Kruskal算法
建立最小生成树的算法之一。整体逻辑是将所有边按照权值大小进行升序排列,然后判断边的两个节点是否已经连接(属于同一个并查集),如果没有连接,那么就进行合并操作,并且将权值进行累加。
算法实现如下:
int kruskal()
{
int res = 0;
sort(node, node + n, [](Node a, Node b){return a.val < b.val; });
for(int i = 0; i < n; i++)
{
if(findx(node[i].from) != findx(node[i].to))
{
merge(node[i].from, node[i].to);
res += node[i].val;
}
}
return res;
}
例题
【问题描述】
连接多个建筑
【输入形式】
办公区域分布图的顶点按照自然数(0,1,2,n-1)进行编号,从标准输入中首先输入两个正整数,分别表示线路图的顶点的数目和边的数目,然后在接下的行中输入每条边的信息,每条边占一行,具体形式如下:
<n> <e>
<id> <vi> <vj> <weight>
…
即顶点vi和vj之间边的权重是weight,边的编号是id。
【输出形式】
输出铺设光缆的最小用料数,然后另起一行输出需要铺设的边的id,并且输出的id值按照升序输出。
【样例输入】
6 10
1 0 1 600
2 0 2 100
3 0 3 500
4 1 2 500
5 2 3 500
6 1 4 300
7 2 4 600
8 2 5 400
9 3 5 200
10 4 5 600
【样例输出】
1500
2 4 6 8 9
【样例说明】
样例输入说明该分布图有6个顶点,10条边;顶点0和1之间有条边,边的编号为1,权重为600;顶点0和2之间有条边,权重为100,其它类推。其对应图如下:
经计算此图的最少用料是1500,可以使图连通,边的编号是2 4 6 8 9。其对应的最小生成树如下:
实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef struct NODE
{
int index;
int from, to;
int val;
} Node;
void init(int n, int m, int *&father, Node *&node)
{
father = new int[n];
node = new Node[m];
for (int i = 0; i < n; i++)
father[i] = i;
}
int findx(int x, int *&father)
{
return father[x] = father[x] == x ? x : findx(father[x], father);
}
void merge(int x, int y, int *&father)
{
int a = findx(x, father);
int b = findx(y, father);
if (a != b)
father[a] = b;
}
inline bool connected(int a, int b, int *&father)
{
return findx(a, father) == findx(b, father);
}
int kruskal(int m, Node *&node, int *&father, vector<int> &v)
{
int res = 0;
sort(node, node + m, [](Node a, Node b) { return a.val < b.val; });
for (int i = 0; i < m; i++)
{
if (!connected(node[i].from, node[i].to, father))
{
merge(node[i].from, node[i].to, father);
res += node[i].val;
v.push_back((node[i].index));
}
}
return res;
}
void del(int *&father, Node *&node, vector<int> &v)
{
delete father;
delete node;
v.clear();
}
int main(int argc, char *argv[])
{
int *father = nullptr;
int n, m, res = 0;
Node *node = nullptr;
vector<int> v;
cin >> n >> m;
init(n, m, father, node);
for (int i = 0; i < m; i++)
cin >> node[i].index >> node[i].from >> node[i].to >> node[i].val;
res = kruskal(m, node, father, v);
cout << res << endl;
sort(v.begin(), v.end());
for (int i : v)
cout << i << " ";
del(father, node, v);
father = nullptr;
node = nullptr;
return 0;
}
代码
#include <bits/stdc++.h>
using namespace std;
typedef struct NODE
{
int from, to, val;
} Node;
int father[105];
int n, m;
Node *node;
void init()
{
for (int i = 0; i <= n; i++)
father[i] = i;
}
int findx(int x)
{
return father[x] = father[x] == x ? x : findx(father[x]);
}
void merge(int x, int y)
{
int a = findx(x);
int b = findx(y);
if (a != b)
father[a] = b;
}
inline bool connected(int a, int b)
{
return findx(a) == findx(b);
}
int kruskal()
{
int res = 0;
sort(node, node + m, [](Node &a, Node &b) { return a.val < b.val; });
for (int i = 0; i < m; i++)
{
if (!connected(node[i].from, node[i].to))
{
merge(node[i].from, node[i].to);
res += node[i].val;
}
}
return res;
}
int main(int argc, char *argv[])
{
cin.tie(NULL);
cout.tie(NULL);
ios::sync_with_stdio(false);
while (cin >> n, n)
{
init();
m = n * (n - 1) >> 1;
node = new Node[m];
for (int i = 0; i < m; i++)
cin >> node[i].from >> node[i].to >> node[i].val;
cout << kruskal() << endl;
}
return 0;
}