力扣-并查集
解决问题:具有传递性的问题,判断两个点是否是联通的、以及路径压缩、按秩合并,不关心中间过程
TIPS:通过使用不同的按秩合并,可以知道节点的个数,和树的高度,增加一个变量n则可以知道进行了多少次连接操作.根据根节点的个数(根节点指向自己)可以求出分组的个数
-
剑指 Offer II 111. 计算除法
我的题解:
思路:因为不同变量之间可能存在传递性,传递性的问题考虑用并查集来解决。我们需要对每个字符串都给一个id,并维护两个数组,分别代表当前字符的父节点,和权值。
初始化时,公式
w e i g h t [ r o o t X ] = v a l ∗ w e i g h t [ y ] / w e i g h t [ x ] weight[rootX] = val * weight[y] / weight[x] weight[rootX]=val∗weight[y]/weight[x]
然后我们根据要查询的结果,去并查集中找,找的过程中,更新父节点和权值,最终让父节点指向根节点,权值为路径的乘积。我们只需要判断两个字符的根节点是否相同就可以判断是否相连,若是则返回
w e i g h t [ a ] / w e i g h t [ b ] weight[a] / weight[b] weight[a]/weight[b]
即可,若不相连返回-1.0package demo04_10; import java.util.HashMap; import java.util.List; import java.util.Map; public class Solution { public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { int equationSize = equations.size(); UnionFind unionFind = new UnionFind(2 * equationSize); Map<String, Integer> map = new HashMap<>(); int id = 0; //第一步,预处理,将变量的值与id进行映射,使得并查集的底层使用数组实现 for (int i = 0; i < equationSize; i++) { String a = equations.get(i).get(0); String b = equations.get(i).get(1); if(!map.containsKey(a)){ map.put(a, id++); } if(!map.containsKey(b)){ map.put(b, id++); } //初始化 unionFind.union(map.get(a), map.get(b), values[i]); } //第二步,做查询 int queriesSize = queries.size(); double[] ans = new double[queriesSize]; for (int i = 0; i < queriesSize; i++) { String a = queries.get(i).get(0); String b = queries.get(i).get(1); Integer id1 = map.get(a); Integer id2 = map.get(b); //若有一个不存在就赋为-1.0 if(!map.containsKey(a) || !map.containsKey(b)){ ans[i] = -1.0; }else{ ans[i] = unionFind.isConnected(id1, id2); } } return ans; } //并查集 private class UnionFind{ private int[] parent; /** * 指向父节点的权值 */ private double[] weight; public UnionFind(int n){ this.parent = new int[n]; this.weight = new double[n]; for (int i = 0; i < n; i++) { parent[i] = i; weight[i] = 1.0; } } //初始化 public void union(int x, int y, double val){ int rootX = find(x); int rootY = find(y); //若两个字符相等则不需要处理 if(rootX == rootY) return; //将rootX的根节点指向rootY parent[rootX] = rootY; //求权值 weight[rootX] = val * weight[y] / weight[x]; } /** * 路径压缩-合并查找 * @param x * @return 根节点的id */ public int find(int x){ //若不相等就一直往根节点找,直到找到根节点,中间权值相乘 if(x != parent[x]){ int origin = parent[x]; parent[x] = find(parent[x]); weight[x] *= weight[origin]; } return parent[x]; } public double isConnected(int x, int y){ //若联通那根节点肯定一样 int rootX = find(x); int rootY = find(y); //若反向相反结果就是倒数 if(rootX == rootY){ return weight[x] weight[y]; }else{ return -1.0; } } } }
-
省份数量
我的题解:
思路: x - y, y - z => x - z,像这种传递性的问题我们使用并查集来解决是比较好的,并查集的优点是,可以在查询的过程中优化路径,将子节点的父节点直接指向根节点。这题是要求省会的个数,因此我们在初始化并查集时,若x,y是联通的我们用find分别找到x的根节点和y的根节点,然后让x的根节点的根节点指向y的根节点,从而实现了该方向的路径压缩。求省会的个数时只需要看有多少城市的根节点指向自己,若指向自己说明自己就是根节点。
public int findCircleNum(int[][] isConnected) { int n = isConnected.length; //最大长度:每一个都不相连 UnionFind unionFind = new UnionFind(n); //遍历数组,完成对其它间接联通的初始化 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if(isConnected[i][j] == 1) unionFind.union(i, j); } } return unionFind.max(); } //并查集 class UnionFind{ int[] parents; int n; //构造方法初始化 public UnionFind(int n){ this.n = n; parents = new int[n]; //初始化 for (int i = 0; i < n; i++) { parents[i] = i; } } //初始化方法 public void union(int x, int y){ //分别查找x,y的根节点 int rootX = find(x); int rootY = find(y); //将x的根节点的根节点指向y的根节点,从而实现x-y方向的路径压缩 parents[rootX] = rootY; } //路径压缩 public int find(int x){ //当该节点和不是根节点时,去找根节点 if(x != parents[x]){ parents[x] = find(parents[x]); } //返回x的根节点 return parents[x]; } //求省会的个数 public int max(){ int max = 0; for (int i = 0; i < n; i++) { if(parents[i] == i) max++; } return max; } }
-
冗余连接
我的题解:
思路: 判断一条边是否是冗余边,只需要判断在这两个点连接之前,这两条边是否已经联通,对应并查集中即是否存在公共的父节点,若是联通的则说明加上该边后会出现环,那么该边就是冗余边,否则就不是冗余边, 至于说返回最后一条冗余边,遇到环直接返回即可,因为连接n个点需要n-1条边,若出现环则肯定是第n条边,即是最后一条直接返回即可
//冗余连接 /* * 我们验证该边是不是冗余边,只需要看这两个点在臊面该边之前是否是联通的,若是联通的则有环出现,即该边式冗余边,否则不是冗余边 * */ int[] parents; public int[] findRedundantConnection(int[][] edges) { int n = edges.length; parents = new int[n + 1]; //初始化,自己指向自己 for (int i = 1; i < n + 1; i++) { parents[i] = i; } for (int i = 0; i < n; i++) { int[] edge = edges[i]; //看当前两个点是否联通 int x = find(edge[0]); int y = find(edge[1]); if(x != y){ union(x, y); }else{ return edge; } } //若没有就返回空 return new int[0]; } //初始化操作,若是联通的就将x的根节点的根节点指向y的根节点 public void union(int x, int y){ int rootX = find(x); int rootY = find(y); parents[rootX] = rootY; } //路径压缩 public int find(int x){ //若x的根节点并非指向自己,就去更新根节点,并将路径进行压缩,使之指向根节点 if(x != parents[x]){ parents[x] = find(parents[x]); } return parents[x]; }
-
连通网络的操作次数
我的题解:
思路:路径连接问题不考虑中间过程,可以使用并查集,并查集中返回未连接点的个数是非常简单的,只需要在合并的过程中加一个参数count即可
1.首先我们对c进行初始化,判断c与n的大小关系,若c < n - 1就说明不可能将这n个点相连,直接返回-1
2.我们初始化并查集,并将相连的边相连,然后我们返回未连接点的个数-1就是需要边的最少个数public int makeConnected(int n, int[][] connections) { int c = connections.length; if(c < n - 1) return -1; UnionFind unionFind = new UnionFind(n); for(int[] connection : connections){ unionFind.union(connection[0], connection[1]); } return unionFind.count - 1; } class UnionFind{ //并查集中可以有三个参数,父亲节点,秩(节点的个数,或树的高度),未联通节点的个数count,每做一次有效连接,即连接之前这两个点未连接,连接后count-1 int[] p; int[] s; int count; UnionFind(int n){ count = n; p = new int[n]; s = new int[n]; for (int i = 0; i < n; i++) { p[i] = i; s[i] = 1; } } int find(int x){ if(x != p[x]){ p[x] = find(p[x]); } return p[x]; } void union(int x, int y){ int rX = find(x), rY = find(y); if(rX == rY) return; count--; if(s[rX] < s[rY]){ p[rX] = rY; s[rY] += s[rX]; }else{ p[rY] = rX; s[rX] += s[rY]; } } }
-
最小体力消耗路径
我的题解:
思路: 我们对数组中的每个点(即每个元素)的联通差值进行排序,是x方向和y方向的,然后逐个将list中的元素加入并查集中,每加入一个边就检查一下头节点(0, 0)和尾节点(m-1, n-1)是否联通,若是联通的返回该边的权值即可(因为权值是排过序的,能保证当前权值就是最小的高度差)
int m, n;
public int minimumEffortPath(int[][] heights) {
//初始化
m = heights.length;
n = heights[0].length;
parent = new int[m * n];
List<int[]> sort = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int id = i * n + j;
parent[id] = id;
if(i + 1 < m){
sort.add(new int[] {id, id + n, Math.abs(heights[i + 1][j] - heights[i][j])});
}
if(j + 1 < n){
sort.add(new int[] {id, id + 1, Math.abs(heights[i][j + 1] - heights[i][j])});
}
}
}
int head = 0, tail = m * n - 1;
//排序
Collections.sort(sort, (o1, o2) -> o1[2] - o2[2]);
//顺序将sort中的元素加入并查集中
for (int i = 0; i < sort.size(); i++) {
int[] arr = sort.get(i);
union(arr[0], arr[1]);
if(find(head) == find(tail)) return arr[2];
}
return 0;
}
int[] parent;
public void union(int x, int y){
parent[find(x)] = parent[find(y)];
}
public int find(int x){
if(x != parent[x]){
parent[x] = find(parent[x]);
}
return parent[x];
}
public int getIndex(int x, int y){
return n * x + y;
}
-
由斜杠划分区域
我的题解:
思路: 我们将一个单元格分割成四个部分,并对三种基本的情况进行判断,分别合并单元格内的小块,以及将相邻的单元格进行合并,这里使用并查集来合并路径,并查集内部维护一个count初始值为4 * n * n每次合并都会减少一个最后返回count即可
/* * 我们将一个单元格分割成四个部分,并对三种基本的情况进行判断,分别合并单元格内的小块,以及将相邻的单元格进行合并,这里使用并查集来合并路径, * 并查集内部维护一个count初始值为4 * n * n每次合并都会减少一个最后返回count即可 * */ public int regionsBySlashes(String[] grid) { int n = grid.length; //初始化并查集 UnionFind unionFind = new UnionFind(4 * n * n); //转化为字符数组 char[][] chars = new char[n][n]; for (int i = 0; i < n; i++) { chars[i] = grid[i].toCharArray(); } for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { int id = 4 * (i * n + j); //判断字符并进行合并 if(chars[i][j] == ' '){ unionFind.union(id, id + 1); unionFind.union(id + 1, id + 2); unionFind.union(id + 2, id + 3); }else if(chars[i][j] == '/'){ unionFind.union(id, id + 3); unionFind.union(id + 1, id + 2); }else{ unionFind.union(id + 1, id); unionFind.union(id + 2, id + 3); } //将相邻的单元格进行合并 //横向-向右合并 if(j + 1 < n){ unionFind.union(id + 1, id + 7); } //纵向-向下合并 if(i + 1 < n){ unionFind.union(id + 2, 4 * ((i + 1) * n + j)); } } } return unionFind.getCount(); } class UnionFind{ int[] parent; int count; public UnionFind(int n) { this.count = n; parent = new int[n]; //初始化自己指向自己 for (int i = 0; i < n; i++) { parent[i] = i; } } //合并,每次合并的时候count-- public void union(int a, int b){ int rootA = find(a); int rootB = find(b); //若原来就已经合并就跳出 if(rootA == rootB) return; //将a指向b parent[rootA] = rootB; count--; } //路径压缩-迭代 public int find(int a){ while(a != parent[a]){ parent[a] = parent[parent[a]]; a = parent[a]; } return parent[a]; } public int getCount(){ return count; } }
-
交换字符串中的元素
我的题解1.0:
思路: 我们通过观察可以交换的pairs对,发现相同下标作为跳板那么该问题就具有了传递性,因为题目说可以在任意次数内交换,且只需要返回最终字典序最小的结果即可
因此我们可以考虑用并查集,这里同时使用路径压缩和按秩合并进行处理,对于输入规模较大的问题时间复杂度比只按路径压缩要快上许多
步骤:
1.首先我们将字符串的下标作为点,然后用并查集进行合并操作
2.然后我们将字符串转化为字符数组,然后遍历该数组,根据它的下标找到它的根节点,并以根节点作为锚创建优先队列,将相同根节点的字符传入优先队列中,从而达到了分组的效果
3.最后我们创建一个StringBuilder对象,通过它来合并字符,我们遍历索引0~n-1,然后找到该点的根节点,并从中取出优先队列中队首的字符,就达到了字典序最小的目的
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
//特判
if(pairs.size() == 0) return s;
int n = s.length();
UnionFind unionFind = new UnionFind(n);
//step1
for(int i = 0; i < pairs.size(); i++){
int idx1 = pairs.get(i).get(0);
int idx2 = pairs.get(i).get(1);
unionFind.union(idx1, idx2);
}
//step2
char[] chars = s.toCharArray();
Map<Integer, Queue<Character>> map = new HashMap<>(n);
for (int i = 0; i < n; i++) {
int root = unionFind.find(i);
//这里有对象机会返回对象,没有就会创建一个对象然后返回
map.computeIfAbsent(root, key -> new PriorityQueue<>()).offer(chars[i]);
}
//step3
StringBuilder stringBuilder = new StringBuilder(n);
for (int i = 0; i < n; i++) {
int parent = unionFind.find(i);
stringBuilder.append(map.get(parent).poll());
}
return stringBuilder.toString();
}
class UnionFind{
int n;
int[] parent;
//秩:存储的是树的高度
int[] rank;
public UnionFind(int n) {
this.n = n;
parent = new int[n];
rank = new int[n];
//初始化parent和rank,parent初始时自己的父节点是自己,rank初始高度为1
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 1;
}
}
public int find(int x){
if(x != parent[x]) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY) return;
//按秩合并和路径压缩
if(rank[rootX] == rank[rootY]){
parent[rootX] = rootY;
rank[rootY]++;
//以高度大的为根节点
}else if(rank[rootX] > rank[rootY]){
parent[rootY] = rootX;
}else{
parent[rootX] = rootY;
}
}
}
我的题解2.0:
思路:这里我们用桶排序来代替哈希表+优先队列的数据结构
1.我们创建一个长度为n,宽为26的二维数组,这样就建立了字符和索引的对应关系,并将可交换的索引位置-即路径放入并查集中。
2.同样的我们在遍历字符串的时候根据索引找出其对应的根节点,将字符存入根节点的相应位置,计数器+1,这样就建立了相应的顺序,且是按照字典顺序递增的。
3.最后我们利用StringBuilder对象重构字符串通过再次遍历索引从0~n-1,根据并查集找出对应的根节点,然后取出计数器大于0的字符,计数器-1,本轮迭代终止,继续直到n-1,最后返回结果即可
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
//特判
if(pairs.size() == 0) return s;
//initialize
int n = s.length();
UnionFind unionFind = new UnionFind(n);
//step1
int[][] hash = new int[n][26];
for(List<Integer> list : pairs){
unionFind.union(list.get(0), list.get(1));
}
//step2
char[] chars = s.toCharArray();
for (int i = 0; i < n; i++) {
int p = unionFind.find(i);
hash[p][chars[i] - 'a']++;
}
//step3
StringBuilder stringBuilder = new StringBuilder(n);
for (int i = 0; i < n; i++) {
int p = unionFind.find(i);
for (int j = 0; j < 26; j++) {
if(hash[p][j] > 0){
stringBuilder.append((char)(j + 'a'));
hash[p][j]--;
break;
}
}
}
return stringBuilder.toString();
}
class UnionFind{
int n;
int[] p;
int[] r;
public UnionFind(int n) {
this.n = n;
p = new int[n];
r = new int[n];
for (int i = 0; i < n; i++) {
p[i] = i;
r[i] = 1;
}
}
int find(int x){
if(x != p[x]){
p[x] = find(p[x]);
}
return p[x];
}
void union(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY) return;
if(r[rootX] == r[rootY]){
p[rootX] = rootY;
r[rootY]++;
}else if(r[rootX] < r[rootY]){
p[rootX] = rootY;
}else{
p[rootY] = rootX;
}
}
}
-
移除最多的同行或同列石头
我的题解:
思路: 移除最多的同行或同列的石头,可以用图的思维来理解,可以移除最多的同行或同列就是将所有联通的点都移除掉,这样ans = 点的个数 - 联通量的个数(按边合并)
步骤:
1.首先我们先实例化UnionFind对象然后将矩阵的的点用并查集对横坐标和纵坐标进行合并操作,这里我们使用哈希表存储对应下标的父节点(因为下标的特殊性)
2.返回: n - getCount()
public int removeStones(int[][] stones) {
int n = stones.length;
//step1
UnionFind unionFind = new UnionFind();
for(int[] arr : stones){
int x = arr[0];
int y = ~arr[1];
unionFind.union(x, y);
}
//step2
//返回的个数是顶点的个数 - 未合并的个数
return n - unionFind.getCount();
}
class UnionFind{
Map<Integer, Integer> parent = new HashMap();
int count = 0;
int find(int x){
//若该点不存在就新建一个点,点的个数+1,并将该点的父节点指向自己
if(parent.get(x) == null){
parent.put(x, x);
count++;
}
if(x != parent.get(x)){
parent.put(x, find(parent.get(x)));
}
return parent.get(x);
}
public int getCount() {
return count;
}
//合并操作,若可以合并-即是互联的,那么计数器-1
void union(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY) return;
parent.put(rootX, rootY);
count--;
}
}
-
账户合并
我的题解:
思路: 根据题意,我们需要将含有相同邮箱的账户进行升序合并,这里核心是相同的邮箱,因此我们建立 邮箱 - 账户id, 以及账户id - 账户名,这样的对应关系
我们可以使用并查集,对相同邮箱账户,进行合并操作,然后我们将相同邮箱的账户,合并到一个账户上,并对该账户进行升序排序,最后重构后返回结果
步骤:
1.我们分别创建两个哈希表,分别表示邮箱 - 邮箱id, 邮箱id - 账户,并用实参对哈希表进行初始化操作
2.我们根据实参中的信息将,含有相同邮箱的利用并查集连接在一起
3.我们创建一个哈希表存储邮箱id-合并后的邮箱
4.我们对哈希表进行重构,重构成list类型,将id所对应的账户名作为第一个元素,排序后的邮箱作为后续元素加入list中,最后返回listclass Solution { public List<List<String>> accountsMerge(List<List<String>> accounts) { int id = 0; //step1 Map<String, Integer> emailForId = new HashMap<>(); Map<Integer, String> idForAccount = new HashMap<>(); for(List<String> account : accounts){ String name = account.get(0); for (int i = 1; i < account.size(); i++) { //若不含有该邮箱,就给该邮箱创建id并就将该邮箱加入该账户 String email = account.get(i); if(!emailForId.containsKey(email)){ emailForId.put(email, id++); idForAccount.put(id - 1, name); } } } //step2 UnionFind unionFind = new UnionFind(id); for(List<String> account : accounts){ int first = emailForId.get(account.get(1)); for (int i = 2; i < account.size(); i++) { int other = emailForId.get(account.get(i)); //合并 unionFind.union(first, other); } } //step3 Map<Integer, List<String>> tmp = new HashMap<>(); for(Map.Entry<String, Integer> entry : emailForId.entrySet()){ String email = entry.getKey(); int idx = entry.getValue(); //获取根节点 int root = unionFind.find(idx); List<String> list = tmp.getOrDefault(root, new ArrayList<>()); //将当前邮箱加入 list.add(email); tmp.put(root, list); } //step4 List<List<String>> ans = new ArrayList<>(); for(Map.Entry<Integer, List<String>> entry : tmp.entrySet()){ int idx = entry.getKey(); List<String> emails = entry.getValue(); Collections.sort(emails); String account = idForAccount.get(idx); List<String> cur = new ArrayList<>(); cur.add(account); cur.addAll(emails); ans.add(cur); } return ans; } class UnionFind{ int[] p; int[] r; public UnionFind(int n) { p = new int[n]; r = new int[n]; for (int i = 0; i < n; i++) { p[i] = i; r[i] = 1; } } int find(int x){ if(x != p[x]){ p[x] = find(p[x]); } return p[x]; } void union(int x, int y){ int rootX = find(x); int rootY = find(y); if(rootX == rootY) return; if(r[rootX] == r[rootY]){ p[rootX] = rootY; r[rootY]++; }else if(r[rootX] < r[rootY]){ p[rootX] = rootY; }else{ p[rootY] = rootX; } } } }
-
打砖块
我的题解:
思路:通过分析该问题发现,顶部的节点与下面的节点具有连通性才会被保留下来,若移除目标元素后,该连通性被打断那么该点后续的元素都会被移除
本次移除的元素就是后续的元素。我们可以用并查集来处理该问题。
1.我们首先创建一个矩阵copy对grid中除了hits中的砖块加入到矩阵相应为止
2.建图,把砖块和砖块的连接关系加入并查集,size表示二维网络的大小,也表示虚拟的屋顶在并查集中的编号
3.按照hits的逆序,在copy中补回砖块,把每一次因为补回砖块而与屋顶相连的砖块的增量记录到ans中class Solution { int m, n; int[] fx = {0, 0, -1, 1}; int[] fy = {-1, 1, 0, 0}; public int[] hitBricks(int[][] grid, int[][] hits) { m = grid.length; n = grid[0].length; //step1 int[][] copy = new int[m][n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { copy[i][j] = grid[i][j]; } } for (int i = 0; i < hits.length; i++) { copy[hits[i][0]][hits[i][1]] = 0; } //step2 int size = m * n; //这里最后一个位置代表屋顶 UnionFind unionFind = new UnionFind(size + 1); //将第一行的砖块与屋顶连接 for (int i = 0; i < n; i++) { if(copy[0][i] == 1){ unionFind.union(i, size); } } //合并其它行的砖块 for (int i = 1; i < m; i++) { for (int j = 0; j < n; j++) { if(copy[i][j] == 1){ //若当前行是砖块,且上方也是砖块就合并 if(copy[i - 1][j] == 1){ unionFind.union(getIdx(i - 1, j), getIdx(i, j)); } //若当前列是砖块,左侧也是砖块就合并 if(j > 0 && copy[i][j - 1] == 1){ unionFind.union(getIdx(i, j - 1), getIdx(i, j)); } } } } //step3 int hitsLen = hits.length; int[] ans = new int[hitsLen]; for (int i = hitsLen - 1; i >= 0 ; i--) { int x = hits[i][0], y = hits[i][1]; //若原来本位置是空的,那么就跳过 if(grid[x][y] == 0) continue; //获取补回前当前有多少砖块与屋顶相连 int prev = unionFind.getNode(size); //若补回的该节点在第一行,要与屋顶相连 if(x == 0) { unionFind.union(y, size); } //四个方向上看,如果相邻的4个方向上有砖块就合并 for (int j = 0; j < 4; j++) { int newX = fx[j] + x; int newY = fy[j] + y; //若在范围内,且有砖块 if(inArea(newX, newY) && copy[newX][newY] == 1){ unionFind.union(getIdx(newX, newY), getIdx(x, y)); } } //补回后有多少砖块与屋顶相连 int cur = unionFind.getNode(size); //更新ans[i] ans[i] = Math.max(0, cur - prev - 1); //补上该方块 copy[x][y] = 1; } return ans; } //是否越界 private boolean inArea(int x, int y){ return x >= 0 && x < m && y >= 0 && y < n; } //将二维坐标转化为一维 private int getIdx(int x, int y){ return x * n + y; } //路径压缩和按秩合并 class UnionFind{ int[] parent; //节点的个数 int[] size; public UnionFind(int n) { parent = new int[n]; size = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; } } //路径压缩 int find(int x){ if(x != parent[x]){ parent[x] = find(parent[x]); } return parent[x]; } //合并 void union(int x, int y){ int rootX = find(x); int rootY = find(y); if(rootX == rootY) return; //合并的同时累加上元素的个数 parent[rootX] = rootY; size[rootY] += size[rootX]; } //获取节点个数 int getNode(int x){ return size[find(x)]; } } }
-
保证图可完全遍历
我的题解:
思路: 我们通过一个个将边加入并查集中,来看加入前后,点的连通性来进行计数,这里我们需要优先考虑加入公共边,然后才是Alice和Bob的边
1.我们先创建并查集common,并利用长度n进行初始化操作,并声明count进行计数
2.我们扫描edges,看当前类型是不是公共边,若是则先看两点是否联通,若是,则不添加count++,否则添加
3.我们将common拷贝两份,分别代表alice和bob,分别看是不是自己的路径边,若是则看加入前两点是否已联通,若是则不添加,count++,否则添加
4.我们看alice和bob所有的联通分量是否相同,若相同的返回count,否则返回-1* @param n * @param edges * @return */ public int maxNumEdgesToRemove(int n, int[][] edges) { //step1 UnionFind common = new UnionFind(n); int count = 0; //step2 for(int[] edge : edges){ if(edge[0] == 3){ int x = edge[1] - 1, y = edge[2] - 1; if(common.isConnected(x, y)) count++; else common.union(x, y); } } //step3 int[] parent = common.getParent(); int[] size = common.getSize(); UnionFind alice = new UnionFind(parent, size); UnionFind bob = new UnionFind(parent, size); for(int[] edge : edges){ if(edge[0] == 1){ int x = edge[1] - 1, y = edge[2] - 1; if(alice.isConnected(x, y)) count++; else alice.union(x, y); } } for(int[] edge : edges){ if(edge[0] == 2){ int x = edge[1] - 1, y = edge[2] - 1; if(bob.isConnected(x, y)) count++; else bob.union(x, y); } } //step4 return alice.allConnected() && bob.allConnected() ? count : -1; } //实现路径压缩和按秩合并,其中按秩合并中的秩是节点的个数 class UnionFind{ int[] parent; int[] size; UnionFind(int n){ parent = new int[n]; size = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = i; } } /** * 实现复制 * @param parent * @return */ UnionFind(int[] parent, int[] size){ int n = parent.length; this.parent = new int[n]; this.size = new int[n]; for (int i = 0; i < n; i++) { this.parent[i] = parent[i]; this.size[i] = size[i]; } } public int[] getParent() { return parent; } public int[] getSize() { return size; } /** * 查询-路径压缩 * @param x * @return */ int find(int x){ if(x != parent[x]){ parent[x] = find(parent[x]); } return parent[x]; } /** * 按秩合并 * @param x * @param y */ void union(int x, int y){ int rootX = find(x); int rootY = find(y); if(rootX == rootY) return; parent[rootX] = rootY; size[rootY] += size[rootX]; } /** * 判断两个点是否相连 * @param x * @param y * @return */ boolean isConnected(int x, int y){ return find(x) == find(y); } /** * 所有点是否联通即父节点是否相同 * @return */ boolean allConnected(){ for (int i = 1; i < parent.length; i++) { if(!isConnected(i - 1, i)) return false; } return true; } }
-
水位上升的泳池中游泳
我的题解:
思路: 根据题意grid中元素的每个值都不一样,且值从0~n*n-1是连续的。因为我们可以新建一个数组,数组的索引代表元素的值,元素的位置代表,数组的值
这样就能从0~n-1连续的将对应位置,遍历到,然后我们向四周进行扩展,若该位置的高度小于或等于该索引,那么就连接,若已经连接就跳过。然后我们看透节点0和末尾节点是否相连
若相连就返回
步骤:
1.我们新建数组idx代表不同高度的位置,并对并查集进行初始化
2.我们从前到后进行遍历,每次遍历时以当前位置为基础,看看四周是否有和当前高度相同和更新的,若有则将这些位置用并查集连接起来(若未连接)
3.最后我们看首位是否相连,若相连就返回该索引class Solution { int n; int[] fx = {0, 0, -1, 1}, fy = {-1, 1, 0, 0}; public int swimInWater(int[][] grid) { n = grid.length; //step1 int[] idx = new int[n * n]; UnionFind unionFind = new UnionFind(n * n); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { idx[grid[i][j]] = getIdx(i, j); } } //step2 for (int i = 0; i < n * n; i++) { int x = idx[i] / n; int y = idx[i] % n; for (int j = 0; j < 4; j++) { int newX = x + fx[j]; int newY = y + fy[j]; if(newX >= 0 && newX < n && newY >= 0 && newY < n && grid[newX][newY] <= i){ unionFind.union(idx[i], getIdx(newX, newY)); //step3 if(unionFind.isConnected(0, n * n - 1)){ return i; } } } } return 0; } class UnionFind{ int[] parent; UnionFind(int n){ parent = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; } } int find(int x){ if(x != parent[x]){ parent[x] = find(parent[x]); } return parent[x]; } void union(int x, int y){ int rootX = find(x); int rootY = find(y); if(rootX == rootY) return; parent[rootX] = rootY; } boolean isConnected(int x, int y){ return find(x) == find(y); } } int getIdx(int x, int y){ return x * n + y; } }
-
839. 相似字符串组
我的题解:
思路: 每一个字符串可以代表一个状态,若一个状态可以转移到另外一个状态,那么这两个点之间有一条边,因为不关心中间过程我们可以用并查集来解决。在并查集中,求组的个数是非常简单的,只需要对根节点指向自己的节点进行计数即可
步骤:
1.初始化m,代表m个点,n代表字符串的长度,初始化并查集
2.枚举任意两个,字符串,看是否存在公共边,若存在就用并查集连接
3.返回组数public int numSimilarGroups(String[] strs) { //step1 int m = strs.length; UnionFind unionFind = new UnionFind(m); //step2 for (int i = 0; i < m - 1; i++) { for (int j = i + 1; j < m; j++) { if(isConnected(strs[i], strs[j])){ unionFind.union(i, j); } } } //step3 return unionFind.getGroup(); } class UnionFind{ int[] p; UnionFind(int n){ p = new int[n]; for (int i = 0; i < n; i++) { p[i] = i; } } int find(int x){ if(x != p[x]){ p[x] = find(p[x]); } return p[x]; } void union(int x, int y){ int rootX = find(x), rootY =find(y); if(rootX == rootY) return; p[rootX] = rootY; } /** * 获得分组的个数,只需要对根节点指向自己的节点进行计数即可,然后返回 * @return */ int getGroup(){ int count = 0; for (int i = 0; i < p.length; i++) { if(p[i] == i) count++; } return count; } } /** * 对字符串中不同的字符进行计数,合法的有为0,或为2 * @param s1 * @param s2 * @return */ boolean isConnected(String s1, String s2){ int len = s1.length(), count = 0; char[] chars1 = s1.toCharArray(), chars2 = s2.toCharArray(); for (int i = 0; i < len; i++) { if(chars1[i] != chars2[i]) count++; } return count == 0 || count == 2; }