并查集
主要解决图论中的连通性问题
基本模板:
关键步骤:
- 初始化将所有节点的父节点都指向自己
- 需要路径压缩降低查询父节点的复杂度
class UnionFind{
int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
public UnionFind(int n){
this.parent = new int[n];
for(int i=0; i<n; i++){
parent[i] = i; // 初始化每个节点的父亲就是自己
}
}
// 查询节点x的顶级父节点
public int find(int x){
if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
else{
// 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
// 从而将路径上的所有节点都统一指向了顶级父节点
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并两节点
public void unite(int x, int y){
int x_father = find(x); // x 的顶级父节点
int y_father = find(y); // y 的顶级父节点
// 建立连接关系
parent[x_father] = y_father;
}
// 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
public boolean isConnected(int x, int y){
int x_father = find(x);
int y_father = find(y);
return x_father == y_father;
}
}
优化模板:
class UnionFind{
int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
int[] size; // 记录每个节点对应的连通分量中的节点总数
int n; // 记录总节点数目
int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量)
public UnionFind(int n){
this.parent = new int[n];
this.size = new int[n];
this.n = n;
this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量)
Arrays.fill(size, 1);
for(int i=0; i<n; i++){
parent[i] = i; // 初始化每个节点的父亲就是自己
}
}
// 查询节点x的顶级父节点
public int find(int x){
if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
else{
// 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
// 从而将路径上的所有节点都统一指向了顶级父节点
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并两节点 (返回是否合并成功)
public boolean unite(int x, int y){
int x_father = find(x); // x 的顶级父节点
int y_father = find(y); // y 的顶级父节点
if(x_father==y_father) return false;
// 建立连接关系(路径压缩)
if(size[x_father] < size[y_father]){ // y对应的连通分量大
parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中
size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可
}else{
parent[y_father] = x_father; // y_parent父节点设置为x_parent
size[x_father] += size[y_father];
}
setCount--; // 连通分量总数减一
return true;
}
// 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
public boolean isConnected(int x, int y){
int x_father = find(x);
int y_father = find(y);
return x_father == y_father;
}
}
经典例题:
-
LeeCode1631. 最小体力消耗路径
-
答案
// 并查集 class Solution { public int minimumEffortPath(int[][] heights) { int m = heights.length; int n = heights[0].length; // 构建图中的连接关系 (int[] = {节点1的id, 节点2的id, 节点1和节点2的高度差}) List<int[]> edges = new ArrayList<>(); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { int id = i * n + j; // 顺序给每个节点确定id if(i>0){ // 竖边 edges.add(new int[]{id, id-n, Math.abs(heights[i][j] - heights[i-1][j])}); } if(j>0){ // 横边 edges.add(new int[]{id, id-1, Math.abs(heights[i][j] - heights[i][j-1])}); } } } // 按照高度差将edges从小到大排序 Collections.sort(edges, (a, b) -> { return a[2] - b[2]; }); // 建立连通性 UnionFind uf = new UnionFind(m * n); // 一共m*n个节点 int ans = 0; for(int[] edge: edges){ // 遍历每条边(最先遍历高度差小的边) int x = edge[0], y = edge[1], v = edge[2]; uf.unite(x, y); // 连接edge对应的两个节点 if(uf.isConnected(0, m*n-1)){ // 判断起点和终点是否已经连通 ans = v; // 当前的v就是目前遇到的最大高度差 return ans; } } return ans; } } class UnionFind{ int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点) int[] size; // 记录每个节点对应的连通分量中的节点总数 int n; // 记录总节点数目 int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量) public UnionFind(int n){ this.parent = new int[n]; this.size = new int[n]; this.n = n; this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量) Arrays.fill(size, 1); for(int i=0; i<n; i++){ parent[i] = i; // 初始化每个节点的父亲就是自己 } } // 查询节点x的顶级父节点 public int find(int x){ if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点 else{ // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点 // 从而将路径上的所有节点都统一指向了顶级父节点 parent[x] = find(parent[x]); } return parent[x]; } // 合并两节点 (返回是否合并成功) public boolean unite(int x, int y){ int x_father = find(x); // x 的顶级父节点 int y_father = find(y); // y 的顶级父节点 if(x_father==y_father) return false; // 建立连接关系(路径压缩) if(size[x_father] < size[y_father]){ // y对应的连通分量大 parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中 size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可 }else{ parent[y_father] = x_father; // y_parent父节点设置为x_parent size[x_father] += size[y_father]; } setCount--; // 连通分量总数减一 return true; } // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同) public boolean isConnected(int x, int y){ int x_father = find(x); int y_father = find(y); return x_father == y_father; } }
-
-
LeeCode765. 情侣牵手
-
答案
// 关键点1: (0,1)为一对情侣,(2,3)为一对情侣。为了方便确定情侣编号,使用人的ID/2表示一对情侣的编号。 // (0,1)/2 = (0,0)表示ID为0和1的情侣属于第0对情侣;(2,3)/2 = (1,1)表示ID为2和3的情侣属于第1对情侣。以此类推 // 关键点2: 某一对情侣(a,b)被拆散了,那么其中一个人一定会被安排到其他的组合,其他组合也一定会安排一个新的人到这个组合中。 // 关键点3: 把一对情侣看作一个节点,假设把一个组合中的人安排到其他组合中,就认为两个组合之间存在一条边。 // 那么很显然,如果某一对情侣被拆散,那么这个节点一定会与其他节点存在边的关系。存在下面两种情况: // 1. 节点1中的一个人安排到了节点2中,节点2中的一个人被安排到节点1中,所以节点1和节点2之间只会存在一条边,且与其他节点之间没有边; // 2. 节点1中的一个人安排到了节点2中,节点2中的一个人被安排到另一个节点中......, 以此往复,直到某一个节点中的一个人被安排到节点1中。此时这些节点就会组成一个环。 // 综上所述,根据上面这个模型,n对情侣(节点)可能会组成多个环、两个节点组成的节点对、以及单个节点(代表没有被拆分的情侣) // 关键点4: 包含m个节点的节点环需要进行m-1次交换,才能让环中的所有情侣牵手; // 包含2个节点的节点对需要进行1次交换,才能让节点对中的所有情侣牵手; // 单个节点不需要交换就可以直接牵手。 // 关键点5: 将n个节点使用并查集建立连接关系,求解每个连通分量上的节点数k,(k-1)就是这个连通分量上所有节点成功牵手需要交换的次数。 class Solution { public int minSwapsCouples(int[] row) { // 建立连接关系 UnionFind uf = new UnionFind(row.length/2); // n表示情侣对数 for(int i=0; i<row.length; i=i+2){ uf.unite(row[i]/2, row[i+1]/2); // 人的ID/2表示情侣(节点)的编号 } // 存放每个连通分量上的顶级父节点 HashSet<Integer> finalFather = new HashSet<>(); for(int i=0; i<row.length/2; i=i+1){ finalFather.add(uf.find(i)); // uf.find(i)查找第i对情侣的顶级父节点 } // 计算交换次数 int result = 0; for(int param: finalFather){ // param表示当前连通分量的顶级父节点 result += uf.size[param] - 1; // uf.size[param]表示当前连通分量上节点总数 } return result; } } class UnionFind{ int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点) int[] size; // 记录每个节点对应的连通分量中的节点总数 int n; // 记录总节点数目 int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量) public UnionFind(int n){ this.parent = new int[n]; this.size = new int[n]; this.n = n; this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量) Arrays.fill(size, 1); for(int i=0; i<n; i++){ parent[i] = i; // 初始化每个节点的父亲就是自己 } } // 查询节点x的顶级父节点 public int find(int x){ if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点 else{ // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点 // 从而将路径上的所有节点都统一指向了顶级父节点 parent[x] = find(parent[x]); } return parent[x]; } // 合并两节点 (返回是否合并成功) public boolean unite(int x, int y){ int x_father = find(x); // x 的顶级父节点 int y_father = find(y); // y 的顶级父节点 if(x_father==y_father) return false; // 建立连接关系(路径压缩) if(size[x_father] < size[y_father]){ // y对应的连通分量大 parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中 size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可 }else{ parent[y_father] = x_father; // y_parent父节点设置为x_parent size[x_father] += size[y_father]; } setCount--; // 连通分量总数减一 return true; } // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同) public boolean isConnected(int x, int y){ int x_father = find(x); int y_father = find(y); return x_father == y_father; } }
-
-
LeeCode2867. 统计树中的合法路径数目
-
答案
// 1. 质数节点连接两个非质数集合,集合数目分别为s1,s2。则质数节点对应的合法路径数=s1+s2+s1*s2 // 质数节点连接1个非质数集合,另一端连接质数或者null,非质数集合数目为s1。则质数节点对应的合法路径数=s1 // 质数节点连接0个非质数集合,两端连接质数或者null。则质数节点对应的合法路径数=0 /** 使用并查集解决 for(遍历每一个连接关系[a,b]){ if(a非质数){ if(b非质数) 合并集合 else 不合并集合,记录b连接的非质数节点a,之后通过a找到a对应的非质数集合 }else{ // a为质数 if(b为质数) 不合并集合 else(b为非质数) 不合并集合,记录a连接的非质数节点b,之后通过b找到b对应的非质数集合 } } */ class Solution { private static final int N = 100001; private static boolean[] isPrime = new boolean[N]; static { Arrays.fill(isPrime, true); isPrime[1] = false; for (int i = 2; i * i < N; i++) { if (isPrime[i]) { for (int j = i * i; j < N; j += i) { isPrime[j] = false; } } } } public long countPaths(int n, int[][] edges) { HashMap<Integer,ArrayList<Integer>> recordPrimeConnect = new HashMap<>(); // 记录所有质数节点连接的非质数节点集合 UnionFind uf = new UnionFind(n+1); for(int[] edge: edges){ int a = edge[0], b = edge[1]; if(!isPrime[a]){ if(!isPrime[b]) uf.unite(a, b); else{ // 不合并集合,记录b连接的非质数节点a,之后通过a找到a对应的非质数集合 ArrayList<Integer> connected = recordPrimeConnect.getOrDefault(b, new ArrayList<Integer>()); connected.add(a); recordPrimeConnect.put(b, connected); } }else{ // a为质数 if(isPrime[b]) continue; else{ // (b为非质数) 不合并集合,记录a连接的非质数节点b,之后通过b找到b对应的非质数集合 ArrayList<Integer> connected = recordPrimeConnect.getOrDefault(a, new ArrayList<Integer>()); connected.add(b); recordPrimeConnect.put(a, connected); } } } // 统计有效路径数目 // 1. 质数节点连接两个非质数集合,集合数目分别为s1,s2。则质数节点对应的合法路径数=s1+s2+s1*s2 // 质数节点连接1个非质数集合,另一端连接质数或者null,非质数集合数目为s1。则质数节点对应的合法路径数=s1 // 质数节点连接0个非质数集合,两端连接质数或者null。则质数节点对应的合法路径数=0 long result = 0; for(Map.Entry<Integer,ArrayList<Integer>> entry: recordPrimeConnect.entrySet()){ // System.out.println(""+ entry.getKey() + "value:"+ entry.getValue()); ArrayList<Integer> connectNotPrime = entry.getValue(); // 记录当前质数节点连接的非质数节点 if(connectNotPrime.size()==0) continue; else if(connectNotPrime.size()>=1){ for(int i=0; i<connectNotPrime.size(); i++){ result += (long)uf.size[uf.find(connectNotPrime.get(i))]; for(int j=i+1; j<connectNotPrime.size(); j++){ long s1 = (long)uf.size[uf.find(connectNotPrime.get(i))]; long s2 = (long)uf.size[uf.find(connectNotPrime.get(j))]; result += s1*s2; } } } } return result; } } class UnionFind{ int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点) int[] size; // 记录每个节点对应的连通分量中的节点总数 int n; // 记录总节点数目 int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量) public UnionFind(int n){ this.parent = new int[n]; this.size = new int[n]; this.n = n; this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量) Arrays.fill(size, 1); for(int i=0; i<n; i++){ parent[i] = i; // 初始化每个节点的父亲就是自己 } } // 查询节点x的顶级父节点 public int find(int x){ if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点 else{ // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点 // 从而将路径上的所有节点都统一指向了顶级父节点 parent[x] = find(parent[x]); } return parent[x]; } // 合并两节点 (返回是否合并成功) public boolean unite(int x, int y){ int x_father = find(x); // x 的顶级父节点 int y_father = find(y); // y 的顶级父节点 if(x_father==y_father) return false; // 建立连接关系(路径压缩) if(size[x_father] < size[y_father]){ // y对应的连通分量大 parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中 size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可 }else{ parent[y_father] = x_father; // y_parent父节点设置为x_parent size[x_father] += size[y_father]; } setCount--; // 连通分量总数减一 return true; } // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同) public boolean isConnected(int x, int y){ int x_father = find(x); int y_father = find(y); return x_father == y_father; } }
-