(最好在电脑下浏览本篇博客...手机上看代码不方便)php
当时学的时候看的一本印度的数据结构书(好像是..有点忘了..反正跟同窗们看的都不同...)...里面把本文提到的全部状况都提到了,我这里只是重复实现,再加上一些我的的理解的图解,最后附上两道并查集的题来帮助理解.java
介绍并查集-> 并查集是一种数据结构, 经常使用于描述集合,常常用于解决此类问题:某个元素是否属于某个集合,或者 某个元素 和 另外一个元素是否同属于一个集合数组
数组里存的数字表明所属的集合。好比arr[4]==1;表明4是第一组。若是arr[7]==1,表明7也是第一组。既然 arr[4] == arr[7] == 1 ,那么说明4 和 7同属于一个集合,数据结构
先初始化一个数组。初始时数组内的值与数组的下角标一致。即每一个数字都自成立一个小组。函数
0属于第0个小组(集合),1属于第1个小组(集合),2属于第2个小组(集合)..........测试
接下来让几个数字进行合并操做,就是组队的过程(合并集合)。合并函数unionElements()介绍:优化
初始状况以下:this
让 5和6进行组队。5里的值就变为6了。含义就是:5放弃了第5小组,加入到了第6小组。5和6属于第6小组。翻译
接下来 让1 和2 进行组队。1的下角标就变为2了。含义就是:1和2都属于第2小组。3d
接下来让 2 3进行组队:2想和3进行组队,2就带着原先的全部队友,加入到了3所在的队伍。看下面arr[1] == arr[2]==arr[3]==3,意思就是1 2 3 都属于第3小组。
接下来 1 和 4 进行组队:1就带着原先全部的队友一块儿加入到4所在的队伍中了。看下面arr[1] == arr[2]==arr[3]==arr[4]==4,意思就是1 2 3 4都属于第4小组。
接下来1 和 5进行组队:1就带着原先全部的队友一块儿加入到5所在的队伍中。5在哪一个队伍呢? 由于arr[5]==6,因此5在第6小组。1就带着全部队友进入了小组6。
看下面arr[1] == arr[2]==arr[3]==arr[4]==arr[5]==arr[6]==6,意思就是1 2 3 4 5 6都属于第6小组。
将这个例子的并查集用树形表示来,以下图所示:
find()函数介绍:怎么求出4属于哪一个集合呢,调用find(4)就行了。find(4)返回的结果是什么呢,其实就是arr[4],也就是6,表示4属于第6小组。
isConnected函数介绍:
判断1和6是否是队友(1 和 6 是否是属于同一个集合):arr[1]==arr[6]可知,是队友(属于同一个集合)
判断1和8是否是队友(1 和 8 是否是属于同一个集合):arr[1] != arr[8]可知,不是队友(不属于同一个集合)
/** * 数组实现并查集,元素内数字表明集合号 */ public class UnionFind { /** * 数组,表示并查集全部元素 */ private int[] id; /** * 并查集的元素个数 */ private int size; /** * 构造一个新的并查集 * * @param size 初始大小 */ public UnionFind(int size) { //初始化个数 this.size = size; //初始化数组,每一个并查集都指向本身 id = new int[size]; for (int i = 0; i < size; i++) { id[i] = i; } } /** * 查看元素所属于哪一个集合 * * @param element 要查看的元素 * @return element元素所在的集合 */ private int find(int element) { return id[element]; } /** * 判断两个元素是否同属于一个集合 * * @param firstElement 第一个元素 * @param secondElement 第二个元素 * @return <code>boolean</code> 若是是则返回true。 */ public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } /** * 合并两个元素所在的集合,也就是链接两个元素 * * @param firstElement 第一个元素 * @param secondElement 第二个元素 */ public void unionElements(int firstElement, int secondElement) { //找出firstElement所在的集合 int firstUnion = find(firstElement); //找出secondElement所在的集合 int secondUnion = find(secondElement); //若是这两个不是同一个集合,那么合并。 if (firstUnion != secondUnion) { //遍历数组,使原来的firstUnion、secondUnion合并为secondUnion for (int i = 0; i < this.size; i++) { if (id[i] == firstUnion) { id[i] = secondUnion; } } } } /** * 本并查集使用数组实现,为了更直观地看清内部数据,采用打印数组 */ private void printArr() { for (int id : this.id) { System.out.print(id + "\t"); } System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind union = new UnionFind(n); System.out.println("初始:"); union.printArr(); System.out.println("链接了5 6"); union.unionElements(5, 6); union.printArr(); System.out.println("链接了1 2"); union.unionElements(1, 2); union.printArr(); System.out.println("链接了2 3"); union.unionElements(2, 3); union.printArr(); System.out.println("链接了1 4"); union.unionElements(1, 4); union.printArr(); System.out.println("链接了1 5"); union.unionElements(1, 5); union.printArr(); System.out.println("1 6 是否链接:" + union.isConnected(1, 6)); System.out.println("1 8 是否链接:" + union.isConnected(1, 8)); } }
上面基本的并查集中,数组里存的内容就是本身所在的小组号,或者能够理解为当前小组的队长号。
上面状况的最后一张图片中。元素1和元素5组队,那么就须要元素1所在队伍的全部成员都把本身的小组号改成新的小组号。伪代码以下:
for (int i = 0; i < 数组size; i++) { if (是否和元素1同属于队伍4) { id[i] = secondUnion;//改成新的队伍号 } }
这样的合并操做过低效了,合并一次就O(n)。因此采用快速Union方式。
原先的数组中存的是小组号(或者队长的编号),而如今数组中存的是本身的‘大哥’的编号。(应该说是父亲结点,和父亲数组,但为了更形象,仍是叫‘大哥’更好理解)。
每一个元素均可以去认一个大哥去保护本身,避免被欺负。只能认一个大哥...不能认多个
初始状况以下:每一个元素里的内容就是本身的下角标(编号)。表示本身就是本身大哥,表示很自由,不从属于任何人。以下图所示
链接5 6 : 后来5号老是受欺负,认6号为大哥,本身的内容就变为6了。以下图所示
链接1 2:后来1号发现本身单着也不行,认2号为大哥,本身的内容就变为2了。以下图所示
链接2 3:后来2号发现本身能力有限,就投奔了3号。
解读一下数组内的含义:arr[1]==2,表示元素1的大哥是2号;arr[2]==3,表示元素2的大哥是3号。因此元素1的老大哥实际上是3号。
链接1和4:1号想和4号成为一个小组,怎么办呢?只须要让本身的‘最终老大哥’加入到4号所在的小组就好了。因此1号就撮合本身的‘最终老大’3号,让3号认4号为大哥。(4号是什么来头呢?4号是4号的‘最终老大’,4号本身就是本身的最终老大)。今后1 2 3 4这些元素都是一家子了。
上面这个状况其实用树形结构表示就更加形象了:
链接1 和 4就是把1所在的根指向4。
链接1 5 : 1号想和5号成为一个小组,怎么办呢?只须要让本身的‘最终老大哥’加入到5号所在的小组就好了。因此1号就撮合本身的‘最终老大’4号,让4号认6号为大哥。(6号是什么来头呢?6号是5号的‘最终老大’)。以下图所示。今后1 2 3 4 5 6这些元素都是一家子了。
find()函数介绍:find函数就是找大哥的函数。怎么求出4属于哪一个集合呢,调用find(1)就行了。find(1)返回的结果是什么呢,其实就是1的‘最终大哥’ 元素4。详细过程见代码。
isConnected函数介绍:
判断1和6是否是队友(1 和 6 是否是属于同一个集合):find(1) 是否等于 find(6),也就是 判断俩元素是不是同一个‘最终大哥’
判断1和8是否是队友(1 和 8 是否是属于同一个集合):find(1) 是否等于 find(8),也就是 判断俩元素是不是同一个‘最终大哥’
/** * 数组模拟树,实现并查集。数组内的元素表示父亲的下角表,至关于指针。 */ public class UnionFind { private int[] parent; private int size; public UnionFind(int size) { this.size = size; parent = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; } } public int find(int element) { while (element != parent[element]) { element = parent[element]; } return element; } public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); if (firstRoot == secondRoot) { return; } parent[firstRoot] = secondRoot; } /** * 本并查集使用数组实现,为了更直观地看清内部数据,采用打印数组 */ private void printArr() { for (int parent : this.parent) { System.out.print(parent + "\t"); } System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind union = new UnionFind(n); System.out.println("初始:"); union.printArr(); System.out.println("链接了5 6"); union.unionElements(5, 6); union.printArr(); System.out.println("链接了1 2"); union.unionElements(1, 2); union.printArr(); System.out.println("链接了2 3"); union.unionElements(2, 3); union.printArr(); System.out.println("链接了1 4"); union.unionElements(1, 4); union.printArr(); System.out.println("链接了1 5"); union.unionElements(1, 5); union.printArr(); System.out.println("1 6 是否链接:" + union.isConnected(1, 6)); System.out.println("1 8 是否链接:" + union.isConnected(1, 8)); } }
其实上面讲的union函数,没有采起合理的手段去进行合并。每次都以secondElement为主,每次合并两个集合都让secondElement的根来继续充当合并以后的根。这样极可能达到线性的链表的状态。
那合并的时候怎么处理更好呢?
好比:有下面两个集合。其中 2 和 6 是两个集合的根。下面要让这两个集合合并,可是,合并以后只能有一个老大啊,到底谁来当呢?
在基于重量的union里,谁的人手多,就由谁来当合并以后的大哥。
2元素有4个手下,再算上本身,那就是5我的。
6元素有2个手下,再算上本身,那就是3我的。
很明显是2元素的人手多,因此2来充当合并以后的根节点。
public class UnionFind { private int[] parent; private int[] weight; private int size; public UnionFind(int size) { this.parent = new int[size]; this.weight = new int[size]; this.size = size; for (int i = 0; i < size; i++) { this.parent[i] = i; this.weight[i] = 1; } } public int find(int element) { while (element != parent[element]) { element = parent[element]; } return element; } public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); //若是已经属于同一个集合了,就不用再合并了。 if (firstRoot == secondRoot) { return; } if (weight[firstRoot] > weight[secondRoot]) { parent[secondRoot] = firstRoot; weight[firstRoot] += weight[secondRoot]; } else {//weight[firstRoot] <= weight[secondRoot] parent[firstRoot] = secondRoot; weight[secondRoot] += weight[firstRoot]; } } private void printArr(int[] arr){ for(int p : arr){ System.out.print(p+"\t"); } System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind union = new UnionFind(n); System.out.println("初始parent:"); union.printArr(union.parent); System.out.println("初始weight:"); union.printArr(union.weight); System.out.println("链接了5 6 以后的parent:"); union.unionElements(5, 6); union.printArr(union.parent); System.out.println("链接了5 6 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 2 以后的parent:"); union.unionElements(1, 2); union.printArr(union.parent); System.out.println("链接了1 2 以后的weight:"); union.printArr(union.weight); System.out.println("链接了2 3 以后的parent:"); union.unionElements(2, 3); union.printArr(union.parent); System.out.println("链接了2 3 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 4 以后的parent:"); union.unionElements(1, 4); union.printArr(union.parent); System.out.println("链接了1 4 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 5 以后的parent:"); union.unionElements(1, 5); union.printArr(union.parent); System.out.println("链接了1 5 以后的weight:"); union.printArr(union.weight); System.out.println("1 6 是否链接:" + union.isConnected(1, 6)); System.out.println("1 8 是否链接:" + union.isConnected(1, 8)); } }
上面介绍的是,当两个集合合并时,谁的重量大,谁就来当合并以后的根。是比之前好多了。但仍是有并查集深度太深的问题。并查集越深,就越接近线性,find函数就越接近O(n)
因此有了这种基于高度的union。合并时,谁的深度深,谁就是新的根。这样集合的深度最可能是最大深度的集合的深度,而不会让深度增长。
好比上面的例子中,元素2的深度是2,元素6的深度是3,按基于重量的union合并后,新的集合深度是4。
可是若是不比重量,而是比高度呢?
那就是6的深度是3,2的深度是2。3大于2, 因此6是新集合的根。看下面图。 能够看到按高度合并后,新的结合的深度并无加深,深度为3,而按基于重量的合并后的高度是4。
其余的地方与前面相似,只是你们可能对这段代码有疑惑。我来画个图讲解一下。
if (height[firstRoot] < height[secondRoot]) { parent[firstRoot] = secondRoot; } else if (height[firstRoot] > height[secondRoot]) { parent[secondRoot] = firstRoot; } else { parent[firstRoot] = secondRoot; height[secondRoot] += 1; }
代码中的if 和 else if两种状况应该好理解。两个集合的高度不同的时候,对它们进行合并,新集合高度确定等于高度大的那个集合的高度。因此高度不用调整。
而两个集合高度相等时,哪一个根来当新集合的根已经无所谓了,只须要让其中一个指向另外一个就行了。而后会发现深度加了一层,因此新集合的根的高度就得+1,看下面图。
public class UnionFind { private int[] parent; private int[] height; int size; public UnionFind(int size) { this.size = size; this.parent = new int[size]; this.height = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; height[i] = 1; } } public int find(int element) { while (element != parent[element]) { element = parent[element]; } return element; } public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); if (height[firstRoot] < height[secondRoot]) { parent[firstRoot] = secondRoot; } else if (height[firstRoot] > height[secondRoot]) { parent[secondRoot] = firstRoot; } else { parent[firstRoot] = secondRoot; height[secondRoot] += 1; } } /* 若是要合并的两个集合高度同样,那么随意选一个做为根 我这里选的是让secondRoot做为新集合的根。 而后secondRoot高度高了一层,因此+1 */ private void printArr(int[] arr){ for(int p : arr){ System.out.print(p+"\t"); } System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind union = new UnionFind(n); System.out.println("初始parent:"); union.printArr(union.parent); System.out.println("初始height:"); union.printArr(union.height); System.out.println("链接了5 6 以后的parent:"); union.unionElements(5, 6); union.printArr(union.parent); System.out.println("链接了5 6 以后的height:"); union.printArr(union.height); System.out.println("链接了1 2 以后的parent:"); union.unionElements(1, 2); union.printArr(union.parent); System.out.println("链接了1 2 以后的height:"); union.printArr(union.height); System.out.println("链接了2 3 以后的parent:"); union.unionElements(2, 3); union.printArr(union.parent); System.out.println("链接了2 3 以后的height:"); union.printArr(union.height); System.out.println("链接了1 4 以后的parent:"); union.unionElements(1, 4); union.printArr(union.parent); System.out.println("链接了1 4 以后的height:"); union.printArr(union.height); System.out.println("链接了1 5 以后的parent:"); union.unionElements(1, 5); union.printArr(union.parent); System.out.println("链接了1 5 以后的height:"); union.printArr(union.height); System.out.println("1 6 是否链接:" + union.isConnected(1, 6)); System.out.println("1 8 是否链接:" + union.isConnected(1, 8)); } }
路径压缩就是处理并查集中的深的结点。实现方法很简单,就是在find函数里加上一句 parent[element] = parent[parent[element]];就行了,就是让当前结点指向本身父亲的父亲,减小深度,同时尚未改变根结点的weight(非根节点的weight改变了无所谓)。
注:只能在基于重量的并查集上改find函数,而不能在基于高度的并查集上采用这种路径压缩。由于路径压缩后根的重量不变,但高度会变,然而高度改变后又不方便从新计算。
代码
public class UnionFind { private int[] parent; private int[] weight; private int size; public UnionFind(int size) { this.parent = new int[size]; this.weight = new int[size]; this.size = size; for (int i = 0; i < size; i++) { this.parent[i] = i; this.weight[i] = 1; } } public int find(int element) { while (element != parent[element]) { parent[element] = parent[parent[element]]; element = parent[element]; } return element; } public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); //若是已经属于同一个集合了,就不用再合并了。 if (firstRoot == secondRoot) { return; } if (weight[firstRoot] > weight[secondRoot]) { parent[secondRoot] = firstRoot; weight[firstRoot] += weight[secondRoot]; } else {//weight[firstRoot] <= weight[secondRoot] parent[firstRoot] = secondRoot; weight[secondRoot] += weight[firstRoot]; } } private void printArr(int[] arr){ for(int p : arr){ System.out.print(p+"\t"); } System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind union = new UnionFind(n); System.out.println("初始parent:"); union.printArr(union.parent); System.out.println("初始weight:"); union.printArr(union.weight); System.out.println("链接了5 6 以后的parent:"); union.unionElements(5, 6); union.printArr(union.parent); System.out.println("链接了5 6 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 2 以后的parent:"); union.unionElements(1, 2); union.printArr(union.parent); System.out.println("链接了1 2 以后的weight:"); union.printArr(union.weight); System.out.println("链接了2 3 以后的parent:"); union.unionElements(2, 3); union.printArr(union.parent); System.out.println("链接了2 3 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 4 以后的parent:"); union.unionElements(1, 4); union.printArr(union.parent); System.out.println("链接了1 4 以后的weight:"); union.printArr(union.weight); System.out.println("链接了1 5 以后的parent:"); union.unionElements(1, 5); union.printArr(union.parent); System.out.println("链接了1 5 以后的weight:"); union.printArr(union.weight); System.out.println("1 6 是否链接:" + union.isConnected(1, 6)); System.out.println("1 8 是否链接:" + union.isConnected(1, 8)); } }
连接:http://acm.hdu.edu.cn/showproblem.php?pid=1213
Today is Ignatius' birthday. He invites a lot of friends. Now it's dinner time. Ignatius wants to know how many tables he needs at least. You have to notice that not all the friends know each other, and all the friends do not want to stay with strangers.
One important rule for this problem is that if I tell you A knows B, and B knows C, that means A, B, C know each other, so they can stay in one table.
For example: If I tell you A knows B, B knows C, and D knows E, so A, B, C can stay in one table, and D, E have to stay in the other one. So Ignatius needs 2 tables at least.
翻译:N我的要坐在桌子上吃饭,可是人们拒绝和陌生人坐在一张桌子上。什么样的不算陌生人呢?主要是朋友的朋友的朋友的.....只要能扯上关系就不算陌生人。能扯上关系就能够坐在一张桌子上。因此至少要准备多少张桌子?
思路:其实就是对并查集进行合并操做,只要俩人认识,就组队。把队组好之后,看最后有多少个组(集合)就好了。最初每一个人都自成一组,因此有多少人就有多少组。可是随着他们组队,每两个组合并成一个组,总的组数就会少1。若是组队的时候发现,他俩已经早就‘扯上关系了’,也就表名他俩早就是一组了,那就不用继续合并了,也就不用再 -1 了。
代码:
class UnionFind { private int[] parent; private int[] weight; private int size;//表明并查集中元素个数 private int groups;//表明并查集中有多少个集合(小组) public UnionFind(int size) { this.parent = new int[size]; this.weight = new int[size]; this.size = size; this.groups = size;//由于初始的时候每一个人自成一组,因此有多少人就有多少组 for (int i = 0; i < size; i++) { this.parent[i] = i; this.weight[i] = 1; } } public int find(int element) { while (element != parent[element]) { parent[element] = parent[parent[element]]; element = parent[element]; } return element; } public boolean isConnected(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); //若是已经属于同一个集合了,就不用再合并了。 if (firstRoot == secondRoot) { return; } if (weight[firstRoot] > weight[secondRoot]) { parent[secondRoot] = firstRoot; weight[firstRoot] += weight[secondRoot]; } else {//weight[firstRoot] <= weight[secondRoot] parent[firstRoot] = secondRoot; weight[secondRoot] += weight[firstRoot]; } //合并 firstElement 和 secondElement 所在的两个组后,就少了一组。 this.groups--; } public int getGroups() { return this.groups; } } public class Main { public static void main(String[] args) { java.util.Scanner scanner = new java.util.Scanner(System.in); int times = scanner.nextInt(); for (int i = 0; i < times; i++) { int size = scanner.nextInt(); UnionFind union = new UnionFind(size); int input = scanner.nextInt(); for (int j = 0; j < input; j++) { //由于测试数据是从1开始,而咱们的并查集是从数组的第0位开始 int first = scanner.nextInt() - 1; int second = scanner.nextInt() - 1; union.unionElements(first, second); } System.out.println(union.getGroups()); } } }
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1232
某省调查城镇交通情况,获得现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间均可以实现交通(但不必定有直接的道路相连,只要互相间接经过道路可达便可)。问最少还须要建设多少条道路?
思路:与上面题思路同样,在并查集中进行合并操做,求出最后剩下多少个组(集合)。这些组之间是互相不可达的。假若有M个组,那其实再须要M-1条连线就能够把他们链接起来了。因此组数 - 1 就是最后答案
代码:
class UnionFind { /** * 记录并查集对应位置的父亲结点位置 */ private int[] parent; /** * 记录并查集对应结点的重量 */ private int[] weight; /** * 表示并查集的元素个数 */ private int size; /** * 表示并查集中集合的个数(组数) */ private int groups; public UnionFind(int size) { this.size = size; this.groups = size; this.parent = new int[size]; this.weight = new int[size]; for (int i = 0; i < size; i++) { this.parent[i] = i; this.weight[1] = 1; } } public int find(int element) { while (element != parent[element]) { parent[element] = parent[parent[element]]; element = parent[element]; } return element; } public boolean isConneted(int firstElement, int secondElement) { return find(firstElement) == find(secondElement); } public void unionElements(int firstElement, int secondElement) { int firstRoot = find(firstElement); int secondRoot = find(secondElement); if (firstRoot == secondRoot) { return; } if (weight[firstRoot] < weight[secondRoot]) { parent[firstRoot] = secondRoot; weight[secondRoot] += weight[firstRoot]; } else { parent[secondRoot] = firstRoot; weight[firstRoot] += secondRoot; } this.groups--; } public int getGroups(){ return this.groups; } } public class Main { public static void main(String[] args) { java.util.Scanner scanner = new java.util.Scanner(System.in); int size = scanner.nextInt(); while(size!=0){ int input = scanner.nextInt(); UnionFind union = new UnionFind(size); for(int i = 0;i<input;i++){ //由于测试数据中是从1开始技术。而咱们的并查集是从0开始,因此每一个输入都减1 int first = scanner.nextInt() - 1; int second = scanner.nextInt() - 1; union.unionElements(first,second); } //最后剩下的组数 - 1 就是最后的答案。由于链接M组的话,须要M-1条连线就能够了 System.out.println(union.getGroups() - 1); size = scanner.nextInt(); } } }