🍕 并查集
岛问题
一个矩阵中只有 0 和 1 两种值, 每个位置都可以和自己的上、下、左、右 四个位置相连, 如果有一片1连在一起, 这个部分叫做一个岛, 求一个矩阵中有多少个岛?
方法很简单:
- 遍历每个元素, 识别到 1 时, 代表碰到一个岛
- 此时对这个岛进行感染, 将该岛的所有元素设置为 0
- 感染完成后继续遍历过程, 如果又遇到一个 1, 说明这个岛肯定是新岛, 再次感染
- 如此反复, 最终就可以求出岛的数量
时间复杂度 O(M*N)
- 遍历时, 每个元素都会访问一次
- 感染过程时, 一个元素最多被访问四次(从上下左右四个方向访问进来)
java
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++;
// 同时感染这个岛, 即属于这个岛的元素, 值将不会是 1
infect(m, i, j, N, M);
}
}
}
return res;
}
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) { // 不是 1, 说明是 "海"
return;
}
m[i][j] = 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);
}
并查集
并查集提供的接口:
- 初始化, 接收一个元素列表, 每个元素初始为单独一个集合
- union, 合并集合。
- is_same_set, 查询是否同一集合。
经典结构实现时难以两全其美:
- 链表结构实现时
- 合并集合时很简单, 只需要将两条链表合并在一起, 时间复杂度是 O(1)
- 但查询是否属于同一集合时, 需要遍历这条链表, 时间复杂度是 O(N)
- 哈希表结构实现时
- 查询是否在同一集合时很快, 时间复杂度是 O(1)
- 合并集合时慢, 因为需要将一个集合的所有元素导入到另一个集合中, 时间复杂度是 O(N)
并查集结构, 可视化出来后就是一个向上指的图。 即每一个元素都指向自己的父元素, 同一个集合中的元素会有一个代表元素。
- 当合并两个集合时, 只需要将一个集合的代表元素指向另一个集合的代表元素就可以了, 时间复杂度 O(1)
- 当查询是否在一个集合, 只需要查询代表元素是否是一个, 就可以了。 时间复杂度是 O(1) 。(数学家都花了好多年证明的)
- 查询过程中有一个优化, 因为合并是将代表元素指向代表元素。 所以查询时需要不断的往上找才能找到代表元素
- 优化: 在往上找的过程中, 保存遍历过的父元素, 找到代表元素后, 重新遍历这些父节点, 将他们都指向代表元素。 这样下次查询时就是 O(1) 级别的。
java
// 一个样本包成一个元素
class Element<V> {
V value;
}
class UnionFindSet<V> {
HashMap<V, Element<V>> elementMap;
// key 是某个元素, value 是该元素的父元素。 父元素一直往上将会到达该集合的代表元素
HashMap<Element<V>, Element<V>> fatherMap;
// key 是某个集合的代表元素, value 是该集合的大小
HashMap<Element<V>, Integer> sizeMap;
// 初始化
UnionFindSet(List<V> list) {
elementMap = new HashMap<>();
fatherMap = new HashMap<>();
sizeMap = new HashMap<>();
for (V value : list) {
Element<V> element = new ELement<V>(value);
elementMap.put(value, element);
fatherMap.put(element, element);
sizeMap.put(element, 1);
}
}
// 找到该集合所在的代表元素, 同时扁平化(查找过程中的节点全部直接指向代表元素)
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;
}
boolean isSameSet(V a, V b) {
// 首先要确保这两个元素是并查集中的元素
if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
// 然后再查看是不是同一个集合里的
return findHead(elementMap.get(a) == findHead(elementMap.get(b)))
}
return false;
}
void union(V a, V b) {
if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
Element<V> aF = findHead();
Element<V> bF = findHead();
if (aF != bF) {
// 不在一个集合, 则将小集合合并到大集合中
Element<V> big = sizeMap.get(aF) >= sizeMap.get(bF) ? aF : bF;
Element<V> small = big == aF ? bF : aF;
fatherMap.put(small, big);
sizeMap.put(big, sizeMap.get(aF) + sizeMap.get(bF));
sizeMap.remove(small);
}
}
}
}
并行解决岛问题
先讨论两个 CPU 的情况, 两个 CPU, 一个负责一块区域, 查询到的岛数量肯定是大于等于实际岛数量的。 多出来的岛, 是因为边界被切掉了, 所以我们的重点在于, 合并区域时, 如何处理边界上的点的信息, 以下图为例:
原始地图如下, 很明显只有一个岛
111111111111111111111111
000000000000000000111111
111111111111111111111111
111111000000000000000000
111111111111111111111111
000000000000000000111111
111111111111111111111111
切割两边后变成下面这样:
111111111111 111111111111
000000000000 000000111111
111111111111 111111111111
111111000000 000000000000
111111111111 111111111111
000000000000 000000111111
111111111111 111111111111
两个 CPU 各自处理一块, 然后会发现, 左边计算出 3 个岛, 右边计算出 2 个岛。 让他们的计算的时候, 为每一个接触岛边界的点标记所在的岛, 比如用 ABCDE 表示
11111111111A D11111111111
000000000000 000000111111
11111111111B D11111111111
111111000000 000000000000
11111111111B E11111111111
000000000000 000000111111
11111111111C E11111111111
在合并的过程中, 为 ABCDE 初始化成并查集, 然后利用边界信息依次合并几个集合:
- 检查 A D 是否一个集合, 不在一个集合, 故合并, 此时岛的数量5-1=4
- 检查 B D 是否是一个集合, 不在一个集合, 故合并, 岛的数量 4-1=3
- 检查 B E 是否在一个集合, 不在一个集合, 故合并, 岛的数量 3-1=2
- 检查 C E 是否在一个集合, 不在一个集合, 故合并, 岛的数量 2-1=1
- 只在合并的时候岛的数量减一, 如果两个点在一个集合, 则不合并, 此时岛的数量也不会减1
这是两个 CPU 的情况, 当多个 CPU 时, 会被划分为更多的区域, 此时就会出现 4 个边界的情况, 处理方式是一样的, 只不过编程起来麻烦点。 但方法要懂。
这其实一个一个 map reduce 过程, 即拆分, 然后合并。