TIPS:通过使用不同的按秩合并,可以知道节点的个数,和树的高度,增加一个变量n则可以知道进行了多少次连接操作.根据根节点的个数(根节点指向自己)可以求出分组的个数
思路:因为不同变量之间可能存在传递性,传递性的问题考虑用并查集来解决。我们需要对每个字符串都给一个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.0
package 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;
}
}
思路: 我们通过观察可以交换的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;
}
}
}
思路:这里我们用桶排序来代替哈希表+优先队列的数据结构
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中,最后返回list
class 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;
}
}
思路: 每一个字符串可以代表一个状态,若一个状态可以转移到另外一个状态,那么这两个点之间有一条边,因为不关心中间过程我们可以用并查集来解决。在并查集中,求组的个数是非常简单的,只需要对根节点指向自己的节点进行计数即可
步骤:
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;
}