并查集(Disjoint Set)
是一种非常精巧而且实用的数据结构,它主要用于处理一些不相交集合的问题
。经典的例子有连通子图、最小生成二叉树Kruskal算法和最近公共祖先等。
通常用“帮派”的例子来说明并查集的应用背景。在一个城市中有n个人,它们分成不同的帮派;给出一些人的关系,例如1号和2号是好朋友,2好和3号是好朋友,那么他们就属于一个帮派;在分析完所有的朋友关系之后,问有多少帮派,每个人属于那个帮派。给出的n可能是10的6次方。
并查集
将编号分别为1~n的那个对象划分为不相交集合,在每个集合中,选择其中某个元素代表表示所在的集合。在这个集合中,实现并查集的操作有初始化,合并,查找。
有n个人一起吃饭,有些人互相认识。认识的人想坐在一起
,不想跟陌生人坐。例如A
认识B,B认识C,那么A,B,C会做在一张桌子上。
给出认识的人,问需要多少张桌子?
#include
using namespace std;
const int num_max = 1025;
int p[num_max];
void init_set(int n) { //初始化集合,所有人都没有关系
if (n > num_max) {
printf("Data error\r\n");
exit(-1);
}
for (int i = 1; i <n+1; i++) {
p[i] = i;
}
}
int find_set(int x) {//查看此人是否和其他人有关系
return x == p[x] ? x : p[x];
}
void union_set(int x, int y) {//这两个有关系的人合并在一卓
x = find_set(x);
y = find_set(y);
if (x != y) p[x] = p[y];//合并
}
int main(void) {
int t, n, m, x, y;
cout << "输入测试次数:";
cin >> t;
while (t--) {
cout << "输入总人数:";
cin >> n;
cout << "输入有关系的人的对数:";
cin >> m;
init_set(n);//初始化集合
for (int i = 0; i < m; i++) {
cout << "输入有关系的人:";
cin >> x >> y;
union_set(x, y);
}
int res = 0;
for (int i = 1; i < n + 1; i++) {
if (p[i] == i) res++;
}
cout << "总共需要" << res << "桌";
}
system("pause");
return 0;
}
在合并元素x和y时先搜索到他们的根结点,然后再合并这两个根结点,即把一个根结点的集改成另一个根结点。这两个根结点的高度不同,如果把高度较小的集合并到较大的集上,能减少树的高度。
#include
using namespace std;
const int num_max = 1024;
int p[num_max + 1];
int height[num_max + 1];
void init_set(int n) {//初始化
if (n > num_max) {
cout << “Data error”;
exit(-1);
}
for (int i = 1; i <= n; i++) {
p[i] = i;
height[i] = 0;
}
}
int find_set(int x) { //查找是否有根结点
return x == p[x] ? x : p[x];
}
void union_set(int x, int y) {//优化合并
x = find_set(x);
y = find_set(y);
if (height[x] == height[y]) {
height[y] = height[x] + 1;
p[x] = p[y];
}
else if (height[x] < height[y]) {
p[x] = p[y];
}
else {
p[y] = p[x];
}
}
int main(void) {
int t, n, m, x, y;
cout << “输入测试次数:”;
cin >> t;
while (t–) {
cout << “输入总人数:”;
cin >> n;
init_set(n);//初始化
cout << “输入有关系的人的对数:”;
cin >> m;
for (int i = 0; i < m; i++) {
cout << “输入有关系的人:”;
cin >> x >> y;
union_set(x, y);
}
int res = 0;
for (int i = 1; i <= n; i++) {
if (i == p[i]) {
res++;
}
}
printf(“一共需要%d桌\r\n”, res);
}
system(“pause”);
return 0;
}
将我们的查询方式进行优化,在上面的find_set函数中,查询元素i所属的集需要搜索路径找到根结点,返回的结果是根结点。这条搜索路劲可能很长。如果在返回的时候顺便把i所属的集改成根节点,那么下次搜索的时候就能在O(1)时间内得到结果了。
#include
using namespace std;
const int num_max = 1024;
int p[num_max + 1];
int height[num_max + 1];
void init_set(int n) {//初始化
if (n > num_max) {
cout << "Data error";
exit(-1);
}
for (int i = 1; i <= n; i++) {
p[i] = i;
height[i] = 0;
}
}
int find_set(int x) { //查找是否有根结点
if (x != p[x]) {
find_set(p[x]); //路径压缩,所有的元素的值都改为其根结点
}
return p[x];
}
void union_set(int x, int y) {//优化合并
x = find_set(x);
y = find_set(y);
if (height[x] == height[y]) {
height[y] = height[x] + 1;
p[x] = p[y];
}
else if (height[x] < height[y]) {
p[x] = p[y];
}
else {
p[y] = p[x];
}
}
int main(void) {
int t, n, m, x, y;
cout << "输入测试次数:";
cin >> t;
while (t--) {
cout << "输入总人数:";
cin >> n;
init_set(n);//初始化
cout << "输入有关系的人的对数:";
cin >> m;
for (int i = 0; i < m; i++) {
cout << "输入有关系的人:";
cin >> x >> y;
union_set(x, y);
}
int res = 0;
for (int i = 1; i <= n; i++) {
if (i == p[i]) {
res++;
}
}
printf("一共需要%d桌\r\n", res);
}
system("pause");
return 0;
}