作者:禅与计算机程序设计艺术
1.背景介绍
在计算机科学中,并查集(Disjoint-Set)是一个用来管理元素集合的数据结构。它是一种动态数据结构,其中的每个元素都是一个单独的对象,它不关心对象之间的相对位置或顺序关系。
在并查集中,主要包含两个操作:
- Make Set(创建集合):将一个元素划入一个新的集合中。
- Find Set(查找集合):查询某个元素所属的集合。
并查集常用于解决“分组”、“判圈”等问题,例如,在一个班级中如何确定同学们所在的小组?在一个网络拓扑图中如何快速判断两个节点是否属于同一个连通分量?
一般来说,并查集要比树状数组更适合处理一些涉及树形结构的问题。比如:在图论领域,利用并查集可以轻松求解最小生成树、连通性检测、最短路径、割边等问题;在计算几何领域,利用并查集可以实现动态凸包的维护、点/边的动态归并等操作。
本文将详细介绍并查集的基础知识,并通过实际例子和动画演示,带领读者了解并查集的工作原理和应用。阅读本文不会对读者编程技巧有要求,但是掌握数据结构和算法的基本知识会非常有帮助。
2.核心概念与联系
2.1 概念
并查集是一种树型的数据结构,由一系列的集合组成。在一开始的时候,每个集合中只有一个元素。当我们要合并两个集合时,我们选择其中一个集合作为根节点,然后把另一个集合中所有的元素都指向这个根节点。这样做的结果是所有元素都会被归并到根节点对应的集合中,它们成为一个新的集合。
用一张图表示就是:
如上图,一开始有三个集合{A,B,C}。要合并集合B和C,我们首先找到集合B的根节点,然后找到集合C的根节点。由于B和C是同一个集合,所以他们的根节点都是集合A的根节点。然后,我们遍历所有集合A中元素的集合,把指向集合B的指针改成指向集合A的指针,这样就把集合B和集合C合并到了集合A中。最终得到的图如下:
2.2 操作
并查集的基本操作有两种:
- 创建集合:创建一个空集合,并将其加入到并查集中。
- 查找集合:找到某一个元素所属的集合。
然后还有一个很重要的操作叫做合并集合:即,将两个集合合并到一个大的集合中。具体方法是:将其中一个集合的根节点变换为另一个集合的根节点。
总结一下,并查集包含以下四个操作:
- Create Set(建立集合): 将一个元素划入一个新的集合中。
- Find Set(查询集合): 查询某个元素所属的集合。
- Union Set(合并集合): 将两个集合合并到一个大的集合中。
- Connected(连通性测试): 判断两个元素是否属于同一个集合。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
3.1 创建集合
新建一个集合:设集合的大小为n,则创建一个新的集合S[n+1]。初始化所有的元素对应的值为-1,说明当前每个元素都还没有归属任何集合。然后创建一个新的集合根节点,该节点编号为0,将其对应的值设置为i,i从1开始计数,表示当前集合的数量。例如,对于集合{A,B,C},则S={0,-1,1,2,-1}.这里根节点对应的集合编号为0,集合A、B、C的编号分别为1、2、3。
3.2 查找集合
查询某一元素所在的集合。设待查询元素的值为u,则将该元素的值更新为find(u),find(u)代表着元素u所属的集合编号,如果u的父亲结点号是负数,说明u已经是根节点,直接返回其本身即可。否则,沿着父亲结点到根节点的路径一直向上,直到到达根节点后,返回根节点的编号。
int find(int u){
if (parent[u]<0){
return u; // 如果u是根节点,直接返回u的编号
}else {
parent[u]=find(parent[u]);// 从父节点往上递归,直到根节点
return parent[u];// 返回根节点的编号
}
}
3.3 合并集合
合并两个集合,即令集合A和集合B成为同一个集合。先将B集合的所有元素标记为A集合的根节点,即将parent[v]=find(a)。然后再将parent[b]=a,即A集合的根节点parent[a]代表着整个集合A。
void unionSet(int a, int b){
int root_a=find(a);
int root_b=find(b);
if (root_a!=root_b){
for (int v=0;v<n;++v){
if (parent[v]==root_b){
parent[v]=root_a; //将集合B中所有元素标记为A集合的根节点
}
}
parent[root_b]=root_a;// 设置集合B的根节点为A集合的根节点
}
}
3.4 连通性测试
判断两元素是否属于同一个集合。先查找两个元素的集合编号,然后比较两个集合编号是否相同。若相同,则两元素属于同一个集合;若不同,则属于不同的集合。
boolean connected(int a, int b){
int root_a=find(a);
int root_b=find(b);
if (root_a==root_b){
return true;
} else {
return false;
}
}
3.5 完整代码
基于以上算法实现的代码如下:
public class DisjointSet {
private int[] parent; // 记录元素所属的集合
private int n; // 元素个数
public DisjointSet(int size) {
this.n = size;
parent = new int[size + 1];
for (int i = 0; i <= size; i++) {
parent[i] = -1; // 初始化所有的元素对应的值为-1
}
}
public void createSet() {
System.out.println("请输入要创建的元素个数:");
int num = input.nextInt();
while (num > n || num < 0) {
System.out.println("输入错误!");
System.out.println("请输入要创建的元素个数:");
num = input.nextInt();
}
System.out.println("请输入" + num + "个要加入的元素:");
for (int i = 1; i <= num; i++) {
insert(input.nextInt());
}
}
/**
* 插入元素
*/
public boolean insert(int data) {
if (!exist(data)) { // 如果不存在该元素,则插入
for (int i = n; i >= 1; i--) {
if (parent[i] == -1) {
parent[i] = data;
break;
}
}
++n; // 集合大小加1
return true;
} else { // 如果存在该元素,则跳过
return false;
}
}
/**
* 删除元素
*/
public boolean delete(int data) {
if (exist(data)) { // 如果存在该元素
--n; // 集合大小减1
for (int i = 1; i <= n; i++) {
if (parent[i]!= -1 && parent[i] == data) {
parent[i] = -1; // 标记为已删除
}
}
return true;
} else { // 不存在该元素
return false;
}
}
/**
* 是否存在该元素
*/
public boolean exist(int data) {
for (int i = 1; i <= n; i++) {
if (parent[i] == data) { // 如果存在该元素,返回true
return true;
}
}
return false; // 如果不存在该元素,返回false
}
/**
* 查找元素所属的集合
*/
public int find(int data) {
if (parent[data] < 0) { // 如果是根节点,直接返回自身
return data;
} else { // 否则,沿着父节点往上递归
parent[data] = find(parent[data]);
return parent[data];
}
}
/**
* 合并两个集合
*/
public boolean merge(int a, int b) {
int root_a = find(a); // 获取集合A的根节点
int root_b = find(b); // 获取集合B的根节点
if (root_a == root_b) { // 如果根节点相同,说明二者已经属于同一个集合
return false; // 此处不进行合并
} else { // 否则,进行合并
for (int i = 1; i <= n; i++) {
if (parent[i] == root_b) { // 如果元素属于集合B,则更改父节点为A
parent[i] = root_a;
}
}
parent[root_b] = root_a; // 修改集合B的根节点为A
return true; // 合并成功
}
}
/**
* 判断两个元素是否属于同一个集合
*/
public boolean connected(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a == root_b) {
return true;
} else {
return false;
}
}
/**
* 获取并查集大小
*/
public int getSize() {
return n;
}
/**
* 打印并查集
*/
public void print() {
System.out.print("\t\tParent\n");
for (int i = 1; i <= n; i++) {
System.out.print("Element-" + i + "\t:\t" + parent[i] + "\n");
}
}
/*
* 测试样例
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
DisjointSet disjset = new DisjointSet(10);
disjset.createSet(); // 创建集合
disjset.print(); // 输出并查集
System.out.println("请输入第一个元素:");
int a = input.nextInt();
System.out.println("请输入第二个元素:");
int b = input.nextInt();
if (disjset.merge(a, b)) { // 合并集合
System.out.println("合并成功!");
} else {
System.out.println("不能合并!");
}
disjset.print(); // 输出并查集
System.out.println("请输入两个元素:");
int c = input.nextInt();
int d = input.nextInt();
if (disjset.connected(c, d)) { // 判断是否属于同一个集合
System.out.println(c + "," + d + "属于同一个集合!");
} else {
System.out.println(c + "," + d + "属于不同的集合!");
}
}
}