并查集Leetcode串题
并查集的核心用途无非就是判断连通区域,具体来说,同一个连通区域的节点都会指向同一个祖先节点,而并查集就是达到这个效果的一个非常简单好用的操作。
首先,我们来熟悉一下并查集的代码,这个代码其实非常简单,主要包含有一个变量ancestors,ancestors[i] = j
表示的是第i个节点的祖先是第j个节点。其中有三个方法:初始化方法、寻找祖先节点方法、连接两个节点方法。我们逐一来看:
class UnionFind{
int[] ancestors;
public UnionFind(int k){
//这里的k表示有k个元素
ancestors = new int[k];
for(int i = 0; i < k; i++){
//初始的时候,每个节点的祖先都是自己
ancestor[i] = i;
}
}
//寻找节点x的祖先节点
public void find(int x){
//寻找祖先节点,无非就是递归查看自己的当前祖先节点,再查看当前祖先节点的祖先节点......
//当x == ancestors[x]时,便可以知道已经到了最顶上的祖先了。
//所以,我们使用while循环
while(x != ancestors[x]){
//这一步进行路径压缩,直接指向自己的”爷爷“节点
ancestors[x] = ancestors[ancestors[x]];
x = ancestors[x];
}
return x;
}
//连接两个节点:当我们惊奇的发现两个节点是属于同一个连通区域的时候,我们可以调用这个函数,意味着这两个原本分离的连通区域,祖先节点都是同一个,一般地,我们将祖先节点定为索引比较小的节点。
public void union(int i, int j){
//寻找二者的祖先节点
i = find(i);
j = find(j);
//将这两个原本分离的连通区域的祖先节点指向同一个(索引较小的节点)
if(i > j){
ancestors[i] = j;
}
else{
ancestors[j] = i;
}
}
}
以上就是最基本的并查集操作,非常简单明了,下面,我们就以leetcode上面的题目,看看我们具体如何在实际题目中思考、使用并查集进行操作。
例题一 547. 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出:2
解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回 2 。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出:1
解释:已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。
提示:
1 <= N <= 200
M[i][i] == 1
M[i][j] == M[j][i]
这道题目中,我们首先应该看到的就是连通区域,虽然没有直接写出来朋友圈就是连通区域,但是,我们可以直观感受到两个不同朋友圈之间相互没有交集,而且,每一个朋友圈中间也可以有一个代表元素作为”祖先节点“。因此在这里,我们快速联想到可以使用并查集的处理方法。题目要我们求的是朋友圈的总数,换句话说,无非要求的就是连通区域的个数,祖先节点的个数。这道题便迎刃而解。
首先,我们可以进行遍历,如果两个学生相应位置为1,说明他们同属于一个朋友圈,我们就应该调用union方法把他们连接起来。遍历完成之后,我们再遍历一次判断存在多少不同的祖先节点就可以了。
注意:并查集的定义大同小异,这里就省略了哈~
class Solution {
public int findCircleNum(int[][] M) {
int len1 = M.length;
if(len1 == 0) return 0;
UnionFind uf = new UnionFind(len1);
for(int i = 0; i < len1; i++){
for(int j = 0; j < len1; j++){
if(M[i][j] == 1){
uf.union(i, j);
}
}
}
HashSet<Integer> set = new HashSet<>();
for(int i = 0; i < len1; i++){
for(int j = 0; j <len1; j++){
if(M[i][j] == 1){
int x = uf.find(i);
set.add(x);
}
}
}
return set.size();
}
我们再看下一道题
例题二 200. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
[
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
输出: 1
示例 2:
输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
这道题中,连通区域的味道也很明显,再矩阵位置上位置联通,意味着他们就应该同属于一个连通区域!而且这道题询问的是岛屿的数量,不就是连通区域的数量嘛!!所以和朋友圈那题大同小异,我们可以火速得到答案。
class Solution {
public int numIslands(char[][] grid) {
int len1 = grid.length;
if(len1 == 0) return 0;
int len2 = grid[0].length;
UnionFind uf = new UnionFind(len1*len2);
for(int i = 0; i < len1; i++){
for(int j = 0; j < len2; j++){
if(grid[i][j] == '1'){
if(i > 0 && grid[i-1][j] == '1'){
uf.union(node(i, j, len2), node(i-1, j, len2));
}
if(j > 0 && grid[i][j-1] == '1'){
uf.union(node(i, j, len2), node(i, j-1, len2));
}
}
}
}
HashSet<Integer> set = new HashSet<>();
for(int i = 0; i < len1; i++){
for(int j = 0; j < len2; j++){
if(grid[i][j] == '1'){
uf.find(node(i, j, len2));
set.add(uf.ancester[node(i, j, len2)]);
}
}
}
return set.size();
}
public int node(int i, int j, int len2){
return (i)*(len2) + j;
}
class UnionFind{
public int[] ancester;
public UnionFind(int k){
ancester = new int[k];
for(int i = 0; i < k; i++){
ancester[i] = i;
}
}
public int find(int x){
while(x != ancester[x]){
ancester[x] = ancester[ancester[x]];
x = ancester[x];
}
return ancester[x];
}
public void union(int i, int j){
i = find(i);
j = find(j);
if(i > j){
ancester[i] = j;
}
else{
ancester[j] = i;
}
}
}
}
例题三 684. 冗余连接
这是一道压轴题,从难度上要比前面的题目稍稍提高。
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
示例 1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
1
/ \
2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
| |
4 - 3
注意:
输入的二维数组大小在 3 到 1000。
二维数组中的整数在1到N之间,其中N是输入数组的大小。
更新(2017-09-26):
我们已经重新检查了问题描述及测试用例,明确图是无向 图。对于有向图详见冗余连接II。对于造成任何不便,我们深感歉意。
这道题要寻找的是树上的一个环路,题目是这样描述的:树中有一条多余的边,多了这条边之后,树出现了环路。而如何判断是否出现环路呢,我们依据之前并查集的知识,如果两个节点出现在同一个环里,那么我们就可以定义这两个节点是联通状态的,而且他们应该要拥有一个相同的祖先节点。
我们设想一下,如果没有出现环路,那么相连接的两个节点不应该有相同的祖先节点;相反,如果两个待连接的节点已经有一个共同的祖先,也反过来可以得出构成了环路。
依据这个思路,我们就可以轻松得到解题思路,连接两个节点之前,我们可以首先查看它们的祖先节点,如果他们已经隶属于同一个祖先节点,那么这条节点就构成了环路。
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
UnionFind uf = new UnionFind(n+1);
int[] error = null;
for(int i = 0; i < n; i++){
if(uf.find(edges[i][0]) == uf.find(edges[i][1])){
error = edges[i];
}
uf.union(edges[i][0], edges[i][1]);
}
return error;
}
class UnionFind{
int[] ancestors;
public UnionFind(int k){
ancestors = new int[k];
for(int i = 0; i < k; i++){
ancestors[i] = i;
}
}
public int find(int x){
while(x != ancestors[x]){
ancestors[x] = ancestors[ancestors[x]];
x = ancestors[x];
}
return x;
}
public void union(int i, int j){
i = find(i);
j = find(j);
if(i > j){
ancestors[i] = j;
}
else{
ancestors[j] = i;
}
}
}
}