并查集
并查集的核心模板是一个数据结构,包含一个数组root[]
,表示每个节点的父亲节点,以及两个方法find()
和union
用于查找和合并。
public class DSU{
int[] root;
public DSU(int n){
root = new int[n];
for(int i=0;i<n;i++)
root[i] = i;
}
public int find(int x){
if(root[x] == x) return x;
//路径压缩,非必须
return root[x] = find(root[x]);
}
public boolean union(int x,int y){
int rootX = root[x];
int rootY = root[y];
//合并,如果两个点的父节点已经一致了,
//说明存在环
if(rootX == rootY) return false;
//合并,如果题目要求可能需要按照集合大小或者其他条件合并。
root[rootX] = rootY;
return true;
}
}
以图判树
题目:https://leetcode-cn.com/problems/graph-valid-tree/
题目大意:
给定从 0 到 n-1 标号的 n 个结点,和一个无向边列表(每条边以结点对来表示),请编写一个函数用来判断这些边是否能够形成一个合法有效的树结构。
示例 1: 输入: n = 5, 边列表 edges = [[0,1], [0,2], [0,3], [1,4]]
输出: true示例 2: 输入: n = 5, 边列表 edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
输出: false
分析:
判断一个图是否为树的要点就在于两个点,这个图是否连通和这个图是否有环。如果一个图强连通并且没有环,说明这个图是一棵树。
那么这道题就比较适合用并查集去做,我们在合并集合的过程中判断是否有环,如果有环则返回false,合并完成之后查看所有节点是否在同一个集合中,如果不是则返回false,其他情况返回true。
class Solution {
public class DSU{
int[] root;
int count;
public DSU(int n){
root = new int[n];
for(int i=0;i<n;i++)
root[i] = i;
count = n;
}
public int find(int x){
//没有压缩路径,
//在leetcode中的额外空间会小一些
//执行时间差不多
while(root[x] != x){
x = root[x];
}
return x;
}
public boolean union(int x,int y){
//记得使用find
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY) return false;
root[rootX] = rootY;
//每合并一个节点就减少一个连通分量
count--;
return true;
}
}
public boolean validTree(int n, int[][] edges) {
DSU dsu = new DSU(n);
for(int[] e:edges){
if(!dsu.union(e[0],e[1]))
return false;
}
//判断连通分量的个数是否为1
return dsu.count == 1;
}
}
按字典序排列最小的等效字符串
题目:https://leetcode-cn.com/problems/lexicographically-smallest-equivalent-string/submissions/
题目大意:
给出长度相同的两个字符串:A 和 B,其中 A[i] 和 B[i] 是一组等价字符。举个例子,如果 A = “abc” 且 B = “cde”,那么就有 ‘a’ == ‘c’, ‘b’ == ‘d’, ‘c’ == ‘e’。
等价字符遵循任何等价关系的一般规则:
自反性:‘a’ == ‘a’
对称性:‘a’ == ‘b’ 则必定有 ‘b’ == ‘a’
传递性:‘a’ == ‘b’ 且 ‘b’ == ‘c’ 就表明 ‘a’ == ‘c’
例如,A 和 B 的等价信息和之前的例子一样,那么 S = “eed”, “acd” 或 “aab”,这三个字符串都是等价的,而 “aab” 是 S 的按字典序最小的等价字符串
利用 A 和 B 的等价信息,找出并返回 S 的按字典序排列最小的等价字符串。
示例 1:
输入:A = “parker”, B = “morris”, S = “parser” 输出:“makkek”
解释:根据 A 和 B 中的等价信息,我们可以将这些字符分为 [m,p], [a,o], [k,r,s], [e,i] 共 4组。每组中的字符都是等价的,并按字典序排列。所以答案是 “makkek”。
示例 2:输入:A = “hello”, B = “world”, S = “hold” 输出:“hdld”
解释:根据 A 和 B 中的等价信息,我们可以将这些字符分为 [h,w], [d,e,o], [l,r] 共 3 组。所以只有 S 中的第二个字符 ‘o’ 变成 ‘d’,最后答案为 “hdld”。
示例 3:扫描二维码关注公众号,回复: 11552863 查看本文章输入:A = “leetcode”, B = “programs”, S = “sourcecode” 输出:“aauaaaaada”
解释:我们可以把 A 和 B 中的等价字符分为 [a,o,e,r,s,c], [l,p], [g,t] 和 [d,m] 共 4 组,因此 S 中除了 ‘u’ 和 ‘d’ 之外的所有字母都转化成了 ‘a’,最后答案为 “aauaaaaada”。
分析:
这道题也是一道很典型的并查集,首先根据给出的两个字符串进行合并,最后再进行查找。
class Solution {
public class DSU{
public int[] root;
public DSU(){
root = new int[26];
for(int i=0;i<26;i++){
root[i] = i;
}
}
public int find(int x){
if(root[x] == x)
return x;
return root[x] = find(root[x]);
}
public void union(int x,int y){
x=find(x);
y=find(y);
if(x==y)
{
return;
}
// 取最小的节点为root节点
if(x>y)
{
root[x]=y;
}else {
root[y]=x;
}
}
}
public String smallestEquivalentString(String A, String B, String S) {
char[] charA = A.toCharArray();
char[] charB = B.toCharArray();
DSU dsu = new DSU();
for(int i=0;i<charA.length;i++){
//合并
dsu.union(charA[i]-'a',charB[i] - 'a');
}
StringBuilder sb = new StringBuilder();
for(int i=0;i<S.length();i++){
char c = S.charAt(i);
//查找
sb.append((char)(dsu.find(c-'a')+'a'));
}
return sb.toString();
}
}
岛屿数量Ⅱ
题目:https://leetcode-cn.com/problems/number-of-islands-ii/
题目大意:
假设你设计一个游戏,用一个 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 岛屿的数量为 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
分析:
这道题也可以使用并查集来做,首先把每个网格看做是一个单独的节点,当一个网格从0变成1之后,首先增加一个连通分量,判断是否可以合并到上下左右四个网格的集合中,如果可以的话就往并查集中加入这条边,并且减少对应的连通分量。
最后判断一下有几个连通分量。
class Solution {
public class DUS{
int[] root;
int count;
public DUS(int m,int n){
int length = m*n;
root = new int[length];
count = 0;
for(int i=0;i<length;i++)
root[i] = i;
}
public int find(int x){
if(root[x] == x)
return x;
return root[x] = find(root[x]);
}
public boolean union(int x,int y){
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY)
return false;
root[rootY] = rootX;
return true;
}
}
public List<Integer> numIslands2(int m, int n, int[][] positions) {
List<Integer> res = new ArrayList<>();
if(m == 0 || n == 0 || positions.length == 0)
return res;
int[][] grid = new int[m][n];
DUS dus = new DUS(m,n);
for(int[] add:positions){
int x = add[0],y = add[1];
if(grid[x][y] == 1){
res.add(dus.count);
continue;
}
//先假设这个节点为单独的节点,增加连通分量
grid[x][y] = 1;
dus.count++;
if(x>0 && grid[x-1][y] == 1){
//加入对应的边,并且减少连通分量
if(dus.union(x*n+y,(x-1)*n+y))
dus.count--;
}
if(y>0 && grid[x][y-1] == 1){
if(dus.union(x*n+y,(x)*n+y-1))
dus.count--;
}
if(x < m-1 && grid[x+1][y] == 1){
if(dus.union(x*n+y,(x+1)*n+y))
dus.count--;
}
if(y < n-1 && grid[x][y+1] == 1){
if(dus.union(x*n+y,x*n+y+1))
dus.count--;
}
res.add(dus.count);
}
return res;
}
}