Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit de3ddaf

Browse filesBrowse files
committed
Add 并查集
1 parent 0bb2b07 commit de3ddaf
Copy full SHA for de3ddaf

File tree

Expand file treeCollapse file tree

3 files changed

+274
-0
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+274
-0
lines changed

‎README.md

Copy file name to clipboardExpand all lines: README.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- [二分搜索](./basic_algorithm/binary_search.md)
2525
- [排序算法](./basic_algorithm/sort.md)
2626
- [动态规划](./basic_algorithm/dp.md)
27+
- [并查集](./basic_algorithm/disjoin_set.md)
2728

2829
### 进阶算法
2930

‎SUMMARY.md

Copy file name to clipboardExpand all lines: SUMMARY.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [二分搜索](basic_algorithm/binary_search.md)
1414
- [排序算法](basic_algorithm/sort.md)
1515
- [动态规划](basic_algorithm/dp.md)
16+
- [并查集](basic_algorithm/disjoin_set.md)
1617

1718
## 进阶算法
1819

‎basic_algorithm/disjoin_set.md

Copy file name to clipboard
+272Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# 并查集
2+
3+
## 简介
4+
5+
并查集用于处理不相交集合的合并与查询问题,常见操作有:
6+
7+
- 查询:查询元素属于哪个集合,可用于判断元素是否在一个集合中
8+
- 合并:合并两个集合
9+
10+
应用场景:动态连通性的判断,且不需要给出具体路径。
11+
12+
## 数据结构
13+
14+
### 初始化
15+
16+
id数组存放的是节点的组号,初始化时将每个节点单独分为一组。
17+
18+
```java
19+
private int[] id;
20+
21+
public DisjoinSet(int size) {
22+
id = new int[size];
23+
for(int i = 0; i < size; i++)
24+
id[i] = i;
25+
}
26+
```
27+
28+
### Quick-Find
29+
30+
由于使用整数表示节点,可以通过数组实现节点到组编号的映射。
31+
32+
```java
33+
public void union(int p, int q) {
34+
// 获得p和q的组号
35+
int pID = id[p];
36+
int qID = id[q];
37+
// 如果两个组号相等,直接返回
38+
if (pID == qID) return;
39+
// 遍历一次,改变组号使他们属于一个组
40+
for (int i = 0; i < id.length; i++)
41+
if (id[i] == pID) id[i] = qID;
42+
count--;
43+
}
44+
```
45+
46+
### Quick-Union
47+
48+
id数组存放的是节点的父节点索引,根节点的父节点是自身,通过这点判断能到达根节点。
49+
50+
```java
51+
private int find(int p) {
52+
// 寻找p节点所在组的根节点,根节点具有性质id[root] = root
53+
while (p != id[p]) p = id[p];
54+
return p;
55+
}
56+
public void union(int p, int q) {
57+
// Give p and q the same root.
58+
int pRoot = find(p);
59+
int qRoot = find(q);
60+
if (pRoot == qRoot)
61+
return;
62+
// 将一棵树(即一个组)变成另外一课树(即一个组)的子树
63+
id[pRoot] = qRoot;
64+
count--;
65+
}
66+
```
67+
68+
### Weighted Quick Union
69+
70+
保存两棵树的大小,每次将小的合并到大的树中。
71+
72+
## 常见例题
73+
74+
### 冗余连接
75+
76+
> [684. 冗余连接](https://leetcode-cn.com/problems/redundant-connection/)
77+
>
78+
> 在本问题中, 树指的是一个连通且无环的**无向**图。
79+
>
80+
> 输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
81+
>
82+
> 结果图是一个以``组成的二维数组。每一个``的元素是一对`[u, v]` ,满足 `u < v`,表示连接顶点`u``v`**无向**图的边。
83+
>
84+
> 返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 `[u, v]` 应满足相同的格式 `u < v`
85+
86+
```java
87+
private int[] parent;
88+
private int[] size;
89+
90+
private int find(int p) {
91+
while (p != parent[p]) {
92+
parent[p] = parent[parent[p]];
93+
p = parent[p];
94+
}
95+
return p;
96+
}
97+
98+
private boolean union(int p, int q) {
99+
int pRoot = find(p);
100+
int qRoot = find(q);
101+
// 在合并前判断是否属于相同的连通分量
102+
if (pRoot == qRoot) {
103+
return true;
104+
}
105+
// Weighted Quick Union
106+
if (size[pRoot] < size[qRoot]) {
107+
parent[pRoot] = qRoot;
108+
size[qRoot] += size[pRoot];
109+
} else {
110+
parent[qRoot] = pRoot;
111+
size[pRoot] += size[qRoot];
112+
}
113+
return false;
114+
}
115+
116+
public int[] findRedundantConnection(int[][] edges) {
117+
parent = new int[edges.length + 1];
118+
size = new int[edges.length + 1];
119+
// 并查集初始化
120+
for (int i = 0; i < parent.length; i++) {
121+
parent[i] = i;
122+
size[i] = 1;
123+
}
124+
for (int[] arr : edges) {
125+
if (union(arr[0], arr[1])) {
126+
// 如果已经连通说明当前这条边是多余的
127+
return arr;
128+
}
129+
}
130+
return new int[]{};
131+
}
132+
```
133+
134+
### 打砖块
135+
136+
> [803. 打砖块](https://leetcode-cn.com/problems/bricks-falling-when-hit/)
137+
>
138+
>有一个 `m x n` 的二元网格,其中 `1` 表示砖块,`0` 表示空白。砖块 **稳定**(不会掉落)的前提是:
139+
>
140+
>- 一块砖直接连接到网格的顶部,或者
141+
>- 至少有一块相邻(4 个方向之一)砖块 **稳定** 不会掉落时
142+
>
143+
>给你一个数组 `hits` ,这是需要依次消除砖块的位置。每当消除 `hits[i] = (rowi, coli)` 位置上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。一旦砖块掉落,它会立即从网格中消失(即,它不会落在其他稳定的砖块上)。
144+
>
145+
>返回一个数组 `result` ,其中 `result[i]` 表示第 `i` 次消除操作对应掉落的砖块数目。
146+
>
147+
>**注意**,消除可能指向是没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。
148+
149+
思路:并查集是用于合并连通分量,而砖块消失实质上是拆分连通分量,因此这题应当逆向考虑,即先打碎所有砖块,再从后向前添加砖块(合并连通分量),添加后计算会增加多少个节点与根节点相连。
150+
151+
首先给出并查集的定义,`size`既表示连通分量的大小,也用于合并时的权重判断。
152+
153+
```java
154+
class DisJoinSet {
155+
156+
private final int[] parent;
157+
private final int[] size;
158+
159+
// 初始化并查集,根节点为自身,大小为1
160+
public DisJoinSet(int len) {
161+
parent = new int[len];
162+
size = new int[len];
163+
for (int i = 0; i < len; i++) {
164+
parent[i] = i;
165+
size[i] = 1;
166+
}
167+
}
168+
169+
// 查找连通分量的根节点
170+
public int find(int p) {
171+
while (p != parent[p]) {
172+
parent[p] = parent[parent[p]];
173+
p = parent[p];
174+
}
175+
return p;
176+
}
177+
178+
// 合并两个节点对应的连通分量
179+
public void merge(int p, int q) {
180+
int pRoot = find(p);
181+
int qRoot = find(q);
182+
// 在合并前判断是否属于相同的连通分量
183+
if (pRoot != qRoot) {
184+
if (size[pRoot] < size[qRoot]) {
185+
parent[pRoot] = qRoot;
186+
size[qRoot] += size[pRoot];
187+
} else {
188+
parent[qRoot] = pRoot;
189+
size[pRoot] += size[qRoot];
190+
}
191+
}
192+
}
193+
194+
// 获取连通分量的大小
195+
public int getSize(int n) {
196+
int root = find(n);
197+
return size[root];
198+
}
199+
200+
}
201+
```
202+
203+
实际使用中将二维数组映射为一维数组,并在最后增加一项作为“房顶节点”,与其相连的节点均不会下落。下面是算法逻辑:
204+
205+
```java
206+
public int[] hitBricks(int[][] grid, int[][] hits) {
207+
int h = grid.length;
208+
int w = grid[0].length;
209+
int[] result = new int[hits.length];
210+
// 保存当前的砖块状态
211+
int[][] status = new int[h][w];
212+
DisJoinSet disJoinSet = new DisJoinSet(h * w + 1);
213+
// 将status初始化为最终的状态
214+
for (int i = 0; i < h; i++) {
215+
status[i] = grid[i].clone();
216+
}
217+
for (int[] pos : hits) {
218+
status[pos[0]][pos[1]] = 0;
219+
}
220+
// 根据最后的状态构造并查集
221+
for (int i = 0; i < h; i++) {
222+
for (int j = 0; j < w; j++) {
223+
if (status[i][j] == 0) {
224+
continue;
225+
}
226+
if (i == 0) {
227+
// 一块砖直接连接到网格的顶部
228+
disJoinSet.merge( h * w, j);
229+
} else {
230+
// 上方有相邻砖块
231+
if (status[i - 1][j] == 1) {
232+
disJoinSet.merge((i - 1) * w + j, i * w + j);
233+
}
234+
// 左侧有相邻砖块
235+
if (j > 0 && status[i][j - 1] == 1) {
236+
disJoinSet.merge(i * w + j - 1, i * w + j);
237+
}
238+
}
239+
}
240+
}
241+
// 从后向前把砖块补上
242+
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
243+
for (int i = hits.length - 1; i >= 0; i--) {
244+
int r = hits[i][0];
245+
int c = hits[i][1];
246+
if (grid[r][c] == 0) {
247+
result[i] = 0;
248+
} else {
249+
// 添加砖块前与房顶相连通的节点数目
250+
int prev = disJoinSet.getSize(h * w);
251+
// 顶部第一行的情况
252+
if (r == 0) {
253+
disJoinSet.merge(c, h * w);
254+
}
255+
// 处理四周的节点
256+
for (int[] direction : directions) {
257+
int nr = r + direction[0];
258+
int nc = c + direction[1];
259+
260+
if (nr >= 0 && nr < h && nc >= 0 && nc < w && status[nr][nc] == 1) {
261+
disJoinSet.merge(r * w + c, nr * w + nc);
262+
}
263+
}
264+
// 获得增加的节点数,即为正向操作时这一步下落的节点数
265+
result[i] = Math.max(0, disJoinSet.getSize(h * w) - prev - 1);
266+
status[r][c] = 1;
267+
}
268+
}
269+
return result;
270+
}
271+
```
272+

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.