一、并查集的概念
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
二、并查集的数据结构以及代码分析
(一)、并查集的数据结构
public class UnionFind {
HashMap<Integer, Integer> father = new HashMap<>();
public UnionFind(Integer n) {
for (int i = 0; i < n; i++) {
father.put(i, i);
}
}
public Integer find(Integer x) {
Integer parent = father.get(x);
while (!parent.equals(father.get(parent))) {
parent = father.get(parent);
}
Integer temp = -1;
Integer fa = father.get(x);
while (!fa.equals(father.get(fa))) {
temp = father.get(fa);
father.put(fa, parent);
fa = temp;
}
return parent;
}
public void union(Integer x, Integer y) {
Integer faX = father.get(x);
Integer faY = father.get(y);
if (!faX.equals(faY)) {
father.put(faX, faY);
}
}
}
(二)、并查集数据结构方法分析
1、通过并查集的概念,我们可以得出我们共需要3种方法
(1)、为每一个元素“i”创建一个新的集合
(2)、返回元素“x”所在集合的根节点
(3)、合并元素“x”所在的集合”A“和元素“y”所在集合”B“两个集合
2、输入数值n,为n以内的每一个元素皆创建一个集合
public UnionFind(Integer n) {
for (int i = 0; i < n; i++) {
father.put(i, i);
}
}
3、两个集合的合并,通过判断元素x与元素y所在的集合是否为同一集合,若不为同一集合,则将x所在的集合合并至y所在的集合内
public void union(Integer x, Integer y) {
Integer faX = father.get(x);
Integer faY = father.get(y);
if (!faX.equals(faY)) {
father.put(faX, faY);
}
}
4、发现元素x所在的集合
(1)、首先不断判断x元素的父节点与其祖父节点(父节点的父节点)是不是同一节点,若为同一节点,则表示找到了最终的根节点,并将parent赋值为根节点。
(2)、将x元素节点以及其往上的所有父辈、祖父辈等节点都将其父节点赋值为根节点。这样以后查找x的父辈等节点的所属集合时,便只需查找一次map便可得到。
(3)、返回根节点
public Integer find(Integer x) {
Integer parent = father.get(x);
while (!parent.equals(father.get(parent))) {
parent = father.get(parent);
}
Integer temp = -1;
Integer fa = father.get(x);
while (!fa.equals(father.get(fa))) {
temp = father.get(fa);
father.put(fa, parent);
fa = temp;
}
return parent;
}
三、小岛问题
(一)、题目需求
假设你设计一个游戏,用一个 m 行 n 列的 2D 网格来存储你的游戏地图。
起始的时候,每个格子的地形都被默认标记为「水」。我们可以通过使用 addLand 进行操作,将位置 (row, col) 的「水」变成「陆地」。
你将会被给定一个列表,来记录所有需要被操作的位置,然后你需要返回计算出来 每次 addLand 操作后岛屿的数量。
注意:一个岛的定义是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。
请仔细阅读下方示例与解析,更加深入了解岛屿的判定。
示例:
输入: m = 3, n = 3, positions = [[0,0], [0,1], [1,2], [2,1]]
输出: [1,1,2,3]
解析:
起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」)
0 0 0
0 0 0
0 0 0
操作 1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。
1 0 0
0 0 0 Number of islands = 1
0 0 0
操作 2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。
1 1 0
0 0 0 岛屿的数量为 1
0 0 0
操作 3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。
1 1 0
0 0 1 岛屿的数量为 2
0 0 0
操作 4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。
1 1 0
0 0 1 岛屿的数量为 3
0 1 0
拓展:
你是否能在 O(k log mn) 的时间复杂度程度内完成每次的计算?(k 表示 positions 的长度)
(二)、解法
- Point
public class Point {
int x;
int y;
public Point() {
this.x = 0;
this.y = 0;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
- NumberOfIsland2
public class NumberOfIsland2 {
class UnionFind {
private HashMap<Integer, Integer> father = new HashMap<Integer, Integer>();
public UnionFind(int row, int column) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
Integer id = convertId(i, j, row);
father.put(id, id);
}
}
}
public Integer convertId(int x, int y, int row) {
return x * row + y;
}
public Integer find(int x) {
Integer parent = father.get(x);
while (!parent.equals(father.get(parent))) {
parent = father.get(parent);
}
int temp = -1;
Integer fa = father.get(x);
while (!fa.equals(father.get(fa))) {
temp = father.get(fa);
father.put(fa, parent);
fa = temp;
}
return parent;
}
public void union(int x, int y) {
Integer faX = father.get(x);
Integer faY = father.get(y);
if (!faX.equals(faY)) {
father.put(faX, faY);
}
}
}
public List<Integer> numIslands(int n, int m, Point[] operators) {
List<Integer> result = new ArrayList<>();
if (operators == null || operators.length == 0) {
return result;
}
int count = 0;
int[] dx = {0, 0, 1, -1};
int[] dy = {1, -1, 0, 0};
UnionFind unionFind = new UnionFind(n, m);
boolean[][] isIsland = new boolean[n][m];
for (Point point : operators) {
int x = point.x;
int y = point.y;
if (!isIsland[x][y]) {
isIsland[x][y] = true;
count++;
Integer id = unionFind.convertId(x, y, n);
for (int k = 0; k < 4; k++) {
int newX = x + dx[k];
int newY = y + dy[k];
if (newX >= 0 && newX < n && newY >= 0 && newY < m && isIsland[newX][newY]) {
Integer newId = unionFind.convertId(newX, newY, n);
Integer father = unionFind.find(id);
Integer nfather = unionFind.find(newId);
if (!father.equals(nfather)) {
count--;
unionFind.union(nfather, father);
}
}
}
}
result.add(count);
}
return result;
}
}
(三)、代码分析
1、并查集的初始化与上面提到的示例有所不同,由于本题中,属于二维坐标,因此单纯以x,或y作为一个位置元素的标识则不可取,于是采取id = x * row + y的形式,为每一个位置元素赋值。
public UnionFind(int row, int column) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
Integer id = convertId(i, j, row);
father.put(id, id);
}
}
}
public Integer convertId(int x, int y, int row) {
return x * row + y;
}
2、定义并初始化上下左右四个方向位置数组。同时使用并查集将每个位置元素创建一个集合。
int count = 0;
int[] dx = {0, 0, 1, -1};
int[] dy = {1, -1, 0, 0};
UnionFind unionFind = new UnionFind(n, m);
boolean[][] isIsland = new boolean[n][m];
3、循环遍历判断每个元素的情况
(1)、判断每个位置元素是否为陆地元素,若为陆地元素,则将陆地数量+1。
(2)、判断该陆地元素上下左右四个方向是否存在其他陆地元素,若存在,则将陆地数量-1。并将其合并至同一集合中。
for (Point point : operators) {
int x = point.x;
int y = point.y;
if (!isIsland[x][y]) {
isIsland[x][y] = true;
count++;
Integer id = unionFind.convertId(x, y, n);
for (int k = 0; k < 4; k++) {
int newX = x + dx[k];
int newY = y + dy[k];
if (newX >= 0 && newX < n && newY >= 0 && newY < m && isIsland[newX][newY]) {
Integer newId = unionFind.convertId(newX, newY, n);
Integer father = unionFind.find(id);
Integer nfather = unionFind.find(newId);
if (!father.equals(nfather)) {
count--;
unionFind.union(nfather, father);
}
}
}
}
result.add(count);
}
return result;