目录
并查集的引入
岛问题
一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个矩阵中有多少个岛?
【举例】 001010
111010
100100
000000
这个矩阵中有三个岛。
public static int countIslands(int[][] m) {
if (m == null || m[0] == null) {
return 0;
}
int N = m.length;//行号
int M = m[0].length;//列号
int res = 0;//岛的数量
for (int i = 0; i < N; i++) {//从左往右
for (int j = 0; j < M; j++) {//从上往下
if (m[i][j] == 1) {//如果当前位置是1
res++;//岛的数量++
infect(m, i, j, N, M);//进入感染过程
}
}
}
return res;
}
public static void infect(int[][] m, int i, int j, int N, int M) {//感染过程
if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1) {
return;
}
m[i][j] = 2;//将所在的位置标记为2
infect(m, i + 1, j, N, M);//右位置感染
infect(m, i - 1, j, N, M);//左位置感染
infect(m, i, j + 1, N, M);//下位置感染
infect(m, i, j - 1, N, M);//上位置感染
}
对于这个程序的时间复杂度为,也就是它的矩阵的规模。
[进阶] 如何设计一个并行算法解决这个问题。
这个问题的意思也就是对于一张非常大的矩阵,利用多个CPU怎么样更快的解决这个问题,我们先思考假如有两个CPU,那么也就是我们把这一张矩阵平均分为两部分,然后在各自部分进行感染操作,统计岛的数量,同时统计边界的信息,左边部分的统计右边界岛的信息,统计属于哪一个岛,右边部分统计左边界岛的信息,统计属于哪一个岛。然后进行两个部分的合并,先把岛的数量加和起来,然后进行边界部分的合并,如果两个边界部分同一行均属于岛的部分,此时两个合并为一个岛,岛的数量减1,依次进行边界部分的合并,最终统计得到岛的数量。
利用同样的方式,如果有N个CPU,对于一张大的矩阵,那么首先将整个大的矩阵均分为N份,在各自的那一份上进行岛的数量的统计,然后四个边界的信息的记录,最终进行岛的合并操作,处理边界部分。这利用的正是并查集的思想。
并查集的介绍
并查集可以将查询两个样本是否在一个集合以及合并集合为一个集合这两个功能的时间复杂度都维持在,对于链表虽然可以时间复杂度
合并两个集合,但是查询两个样本是否在一个集合需要
的时间复杂度,哈希表虽然查询两个集合是否在一个集合可以保持在
的时间复杂度,但是合并两个集合为一个集合却需要
的时间复杂度。并查集则是很好的解决了这个问题。
public class Code_UnionFind {
public static class Element<V> {//对用户所给的表上面包了一层
public V value;
public Element(V value) {
this.value = value;
}
}
public static class UnionFindSet<V> {
public HashMap<V, Element<V>> elementMap;//建立一个表,给一个样本可以找到它加工出来的表面包一层的东西
public HashMap<Element<V>, Element<V>> fatherMap;//建立一个表,key代表某个元素,value代表该元素的父亲
public HashMap<Element<V>, Integer> rankMap;//建立一个表,key代表某个集合的代表元素,value代表该集合的大小
public UnionFindSet(List<V> list) {//并查集初始化时,要求用户将样本都给你
elementMap = new HashMap<>();
fatherMap = new HashMap<>();
rankMap = new HashMap<>();
for (V value : list) {
Element<V> element = new Element<V>(value);
elementMap.put(value, element);//每一个元素有它对应的自己的处理过的元素结构
fatherMap.put(element, element);//初始时,每个元素的父就是自己
rankMap.put(element, 1);//任何一个元素都是代表元素
}
}
//给定一个元素,往上一直找,把代表元素返回
private Element<V> findHead(Element<V> element) {
Stack<Element<V>> path = new Stack<>();
while (element != fatherMap.get(element)) {//如果所给的元素不是它的父亲
path.push(element);//往上走的过程中,沿途经过的节点全部压入栈中
element = fatherMap.get(element);//一直往上走
}
while (!path.isEmpty()) {//这个操作把链打扁平,沿途节点直接指向父亲节点,对于后续的操作更省时间
fatherMap.put(path.pop(), element);//把沿途经过的元素的父亲节点直接设置为最顶部
}
return element;
}
public boolean isSameSet(V a, V b) {//两个集合是否为同一个集合
if (elementMap.containsKey(a) && elementMap.containsKey(b))//已经初始化过 {
return findHead(elementMap.get(a)) == findHead(elementMap.get(b));//分别找到它们两个元素的代表元素
}
return false;
}
public void union(V a, V b) {//两个集合合并为一个集合
if (elementMap.containsKey(a) && elementMap.containsKey(b)) {//已经初始化过
Element<V> aF = findHead(elementMap.get(a));//找到a的父亲
Element<V> bF = findHead(elementMap.get(b));//找到b的父亲
if (aF != bF) {//如果替它们的父亲不相同,进行集合合并
Element<V> big = rankMap.get(aF) >= rankMap.get(bF) ? aF : bF;//返回a的父亲集合和b的父亲集合中,元素较多的哪一个集合的父亲
Element<V> small = big == aF ? bF : aF;//记录较小的那个集合的父亲
fatherMap.put(small, big);//把较小集合的父亲节点改到较大集合的父亲节点下
rankMap.put(big, rankMap.get(aF) + rankMap.get(bF));//总集合的大小更新
rankMap.remove(small);//在代表节点集合中,删掉small节点
}
}
}
}
}