并查集

这部分主要是学习了 labuladong 公众号中对于并查集的讲解,文章链接如下:

Union-Find 并查集算法详解

Union-Find 算法怎么应用?

概述

并查集用于解决图论中「动态连通性」问题;

主要实现以下几个API

class UF {
/* 将 p 和 q 连接 */
public void union(int p, int q);
/* 判断 p 和 q 是否连通 */
public boolean connected(int p, int q);
/* 返回图中有多少个连通分量 */
public int count();
}

关于上述提到的连通性,其具有以下三个特性:

  1. 自反性:节点pp是连通的;
  2. 对称性:如果节点pq连通,那么qp也连通。
  3. 传递性:如果节点pq连通,qr连通,那么pr也连通。

案例代码

class UF {
// 连通分量个数
private int count;
// 存储一棵树,用于记录节点x的父节点
private int[] parent;
// 记录树的“重量”,出于平衡的目的
private int[] size; // 构造函数,n为图的节点总数
public UF(int n) {
// 初始状态:节点之间互不连通
this.count = n;
// 父节点指向自身
parent = new int[n];
// 重量都为1
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
} // 连通p节点与q节点
public void union(int p, int q) {
int rootP = find(p); // 找到p节点的父节点
int rootQ = find(q); // 找到q节点的父节点
if (rootP == rootQ)
return; // 小树接到大树下面,较平衡
if (size[rootP] > size[rootQ]) {
// 让小树rootQ指向大树rootP
parent[rootQ] = rootP;
// 大树的尺寸要加上小树的尺寸
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
// 连通量-1
count--;
} // 判断p节点与q节点是否相连
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
} // 获取此时连通量的个数
public int getCount(){
return this.count;
} // 找x节点的父节点:带路径压缩的
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩,使得整体的复杂度都降至O(1)
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
} // // 找x节点的父节点:不带路径压缩的
// private int find(int x) {
// // 根节点的 parent[x] == x
// while (parent[x] != x){
// x = parent[x];
// }
// return x;
// }
}

题目

130. 被围绕的区域

更好的解法是通过dfs进行求解

但这里也可以用并查集的解法去求解,虽然代码很长,效率不高,但是不失为一种解法

class Solution {

    // 并查集解题
public void solve(char[][] board) { int m = board.length;
int n = board[0].length; // 给 dummy 留一个额外位置
UF uf = new UF(m*n + 1);
int dummy = m*n; // 将首列和末列的 O 与 dummy 连通
for(int i=0; i<m; i++){
if(board[i][0] == 'O'){
uf.union(i*n, dummy);
}
if(board[i][n-1] == 'O'){
uf.union(i*n + n-1, dummy);
}
} // 将首行和末行的 O 与 dummy 连通
for(int j=0; j<n; j++){
if(board[0][j] == 'O'){
uf.union(j, dummy);
}
if(board[m-1][j] == 'O'){
uf.union((m-1)*n + j, dummy);
}
} // 方向数组d是上下左右搜索的常用手法
int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}};
for(int i=1; i<m-1; i++){
for(int j=1; j<n-1; j++){
if(board[i][j] == 'O'){
// 上
if(board[i-1][j] == 'O') uf.union((i-1)*n + j , i*n + j);
// 下
if(board[i+1][j] == 'O') uf.union((i+1)*n + j , i*n + j);
// 左
if(board[i][j-1] == 'O') uf.union(i*n + (j-1) , i*n + j);
// 右
if(board[i][j+1] == 'O') uf.union(i*n + (j+1) , i*n + j);
}
}
} // 所有不和 dummy 连通的 O,都要被替换
for(int i=1; i<m-1; i++){
for(int j=1; j<n-1; j++){
if(!uf.connected(i*n + j, dummy)){
board[i][j] = 'X';
}
}
}
} class UF{
// ... 见上方案例代码
}
}

990. 等式方程的可满足性

boolean equationsPossible(String[] equations) {
// 26 个英文字母
UF uf = new UF(26);
// 先让相等的字母形成连通分量
for (String eq : equations) {
if (eq.charAt(1) == '=') {
char x = eq.charAt(0);
char y = eq.charAt(3);
uf.union(x - 'a', y - 'a');
}
}
// 检查不等关系是否打破相等关系的连通性
for (String eq : equations) {
if (eq.charAt(1) == '!') {
char x = eq.charAt(0);
char y = eq.charAt(3);
// 如果相等关系成立,就是逻辑冲突
if (uf.connected(x - 'a', y - 'a'))
return false;
}
}
return true;
}

200. 岛屿数量

public int numIslands(char[][] grid) {

    int m = grid.length;
int n = grid[0].length; UF uf = new UF(m*n + 1);
int water = m*n; for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(grid[i][j] == '0'){
uf.union(i*n + j, water);
}else{
// 上下左右
if(i-1 >=0 && grid[i-1][j] == '1'){
uf.union(i*n+j, (i-1)*n+j);
}
if(i+1 < m && grid[i+1][j] == '1'){
uf.union(i*n+j, (i+1)*n+j);
}
if(j-1 >=0 && grid[i][j-1] == '1'){
uf.union(i*n+j, i*n+j-1);
}
if(j+1 < n && grid[i][j+1] == '1'){
uf.union(i*n+j, i*n+j+1);
}
}
}
} return uf.getCount() - 1; } class UF {
// ... 见上方案例代码
}

547. 省份数量

// 并查集开整
public int findCircleNum(int[][] isConnected) { int m = isConnected.length; UF uf = new UF(m); for(int i=0; i<m; i++){
for(int j=0; j<i; j++){
if(isConnected[i][j] == 1)
uf.union(i, j);
}
}
return uf.getCount();
} class UF {
// ... 见上方案例代码
}

684. 冗余连接

public int[] findRedundantConnection(int[][] edges) {

    UF uf = new UF(edges.length+1);

    int[] res = new int[2];
for(int i=0; i<edges.length; i++){ if(!uf.connected(edges[i][0], edges[i][1])){
// 当两个节点还没有相连时,则让其相连
uf.union(edges[i][0], edges[i][1]);
}else{
// 当存在新的连接已经有共同的父节点了,说明这个连接会使树成环
res[0] = edges[i][0];
res[1] = edges[i][1];
}
} return res;
} class UF {
// ... 见上方案例代码
}

1319. 连通网络的操作次数

public int makeConnected(int n, int[][] connections) {

    UF uf = new UF(n);

    int times = 0;
for(int i=0; i<connections.length; i++){
if(!uf.connected(connections[i][0], connections[i][1])){
// 没连接,则连接
uf.union(connections[i][0], connections[i][1]);
}else{
// 已经连接了,则不需要再连接,这次连接的线后续可以用
times ++;
}
}
// 还需要连接的计算机群
int need_link = uf.getCount() - 1;
// 判断省下的线是否能满足连接需求来返回相应的结果
return need_link <= times ? need_link : -1;
} class UF {
// ... 见上方案例代码
}

778. 水位上升的泳池中游泳

// 自己做的:一顿操作猛如虎,真TMD击败5%
public int swimInWater(int[][] grid) { int n = grid.length;
UF uf = new UF(n*n); // 由于每个格子的值都不重复,则记录一下起点与终点
int s = grid[0][0];
int e = grid[n-1][n-1];
// 计算初始水位
int t = Math.max(s, e); // 遍历水位,合并可以联通的区域
for(int i=t; i<n*n; i++){
for(int r=0; r<n; r++){
for(int c=0; c<n; c++){
if(grid[r][c] <= i){
// 上下左右
if(r-1 >=0 && grid[r-1][c] <= i)
uf.union(grid[r][c], grid[r-1][c]);
if(r+1 < n && grid[r+1][c] <= i)
uf.union(grid[r][c], grid[r+1][c]);
if(c-1 >=0 && grid[r][c-1] <= i)
uf.union(grid[r][c], grid[r][c-1]);
if(c+1 < n && grid[r][c+1] <= i)
uf.union(grid[r][c], grid[r][c+1]);
// 其他四个方向... 这里为什么不需要不是说可以从游到相邻任意的嘛
// if(r-1 >=0 && c-1 >=0 && grid[r-1][c-1] <= i)
// uf.union(grid[r][c], grid[r-1][c-1]);
// if(r-1 >=0 && c+1 < n && grid[r-1][c+1] <= i)
// uf.union(grid[r][c], grid[r-1][c+1]);
// if(r+1 <n && c-1 >=0 && grid[r+1][c-1] <= i)
// uf.union(grid[r][c], grid[r+1][c-1]);
// if(r+1 <n && c+1 < n && grid[r+1][c+1] <= i)
// uf.union(grid[r][c], grid[r+1][c+1]);
} if(uf.connected(s, e)){
return i;
}
}
}
} return -1;
} // 看了题解之后的优化思路:从0开始找到对应的位置填充
public int swimInWater(int[][] grid) { int n = grid.length;
UF uf = new UF(n*n); // 由于每个格子的值都不重复,则记录一下起点与终点
int s = grid[0][0];
int e = grid[n-1][n-1]; // 记录一下初始值,下标:对应grad[i][j],值:对应grad[i][j]的索引
int[] idxs = new int[n*n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
idxs[grid[i][j]] = i*n + j;
}
} // 开始合并
for(int i=0; i<n*n; i++){
int r = idxs[i] / n;
int c = idxs[i] % n; // 上下左右
if(r-1 >=0 && grid[r-1][c] <= i)
uf.union(grid[r][c], grid[r-1][c]);
if(r+1 < n && grid[r+1][c] <= i)
uf.union(grid[r][c], grid[r+1][c]);
if(c-1 >=0 && grid[r][c-1] <= i)
uf.union(grid[r][c], grid[r][c-1]);
if(c+1 < n && grid[r][c+1] <= i)
uf.union(grid[r][c], grid[r][c+1]); if(uf.connected(s, e)){
return i;
}
} return -1;
}

721. 账户合并

// 这题完全被建立缓存给搞崩了
public List<List<String>> accountsMerge(List<List<String>> accounts) { // 简历n个用户的并查集
int n = accounts.size();
UF uf = new UF(n); // 搞个缓存,来缓存所有的邮箱,即 email->id
Map<String, Integer> emailToId = new HashMap<>();
for(int i=0; i<n; i++){
int size = accounts.get(i).size();
for(int j=1; j<size; j++){
emailToId.put(accounts.get(i).get(j), i);
}
} // 开始合并
for(int i=0; i<n; i++){
// 账户中邮箱的数量
int size = accounts.get(i).size();
// 取出每一个邮箱
for(int j=1; j<size; j++){
String account = accounts.get(i).get(j);
if(emailToId.containsKey(account)){
uf.union(emailToId.get(account), i);
}
}
} // 再搞个缓存 id 对应 email
Map<Integer, List<String>> idToEamil = new HashMap<>();
for(Map.Entry<String, Integer> entry: emailToId.entrySet()){
// 通过并查集使得相同的账户都对应同一个id
int id = uf.find(entry.getValue());
List<String> emails = idToEamil.getOrDefault(id, new ArrayList<>());
// 添加账户
emails.add(entry.getKey());
idToEamil.put(id, emails);
} // 通过上面的步骤,现在的id和email都已经对应了,且id是唯一的
List<List<String>> res = new ArrayList<>();
for(Map.Entry<Integer, List<String>> entry: idToEamil.entrySet()){
// 排序
List<String> emails = entry.getValue();
Collections.sort(emails);
List<String> temp = new ArrayList<>();
// 添加用户名
temp.add(accounts.get(entry.getKey()).get(0));
temp.addAll(emails);
res.add(temp);
} return res;
}

399. 除法求值

u1s1 这题真有点难...

public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {

    int equationsSize = equations.size();
// 缓存的建立,key:'除数/被除数' value:id
Map<String, Integer> cache = new HashMap<>(); // 将除数/被除数 添加到缓存中与 id 一一对应
int idx = 0;
for(List<String> equation: equations){
String str1 = equation.get(0);
String str2 = equation.get(1);
if(!cache.containsKey(str1)){
cache.put(str1, idx++);
}
if(!cache.containsKey(str2)){
cache.put(str2, idx++);
}
} // 建立并查集,其中的节点个数,就是不同的除数被除数的个数
UF uf = new UF(idx--);
idx = 0;
// 再次遍历等式关系,进行union操作
for(List<String> equation: equations){
String str1 = equation.get(0);
String str2 = equation.get(1);
double value = values[idx++];
int p = cache.get(str1);
int q = cache.get(str2);
uf.union(p, q, value);
} // 根据建立的并查集来进行值的判断操作
double[] res = new double[queries.size()];
idx = 0;
for(List<String> query: queries){
String str1 = query.get(0);
String str2 = query.get(1);
if(!cache.containsKey(str1) || !cache.containsKey(str2)){
res[idx++] = -1.0;
}else{
int p = cache.get(str1);
int q = cache.get(str2);
res[idx++] = uf.connected(p, q);
}
} return res;
} public static void main(String[] args) { List<List<String>> equations = new ArrayList<>();
double[] values = new double[]{3.4, 1.4, 2.3};
List<List<String>> queries = new ArrayList<>(); equations.add(Arrays.asList("a", "b"));
equations.add(Arrays.asList("e", "f"));
equations.add(Arrays.asList("b", "e"));
queries.add(Arrays.asList("b", "a"));
queries.add(Arrays.asList("a", "f"));
queries.add(Arrays.asList("f", "f"));
queries.add(Arrays.asList("f", "e")); double[] doubles = new Solution().calcEquation(equations, values, queries);
System.out.println(Arrays.toString(doubles));
} class UF {
// 连通分量个数
private int count;
// 存储一棵树,用于记录节点x的父节点
private int[] parent;
// 原先的size,这里需要修改为权重信息
private double[] weights; // 构造函数,n为图的节点总数
public UF(int n) {
// 初始状态:节点之间互不连通
this.count = n;
// 父节点指向自身
parent = new int[n];
// 重量都为1
weights = new double[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
// 初始的权重都是1.0
weights[i] = 1.0;
}
} // 连通p节点与q节点
public void union(int p, int q, double weight) {
int rootP = find(p); // 找到p节点的父节点
int rootQ = find(q); // 找到q节点的父节点
if (rootP == rootQ)
return; parent[rootQ] = rootP; // 这里必须是q指向p,即除数指向了被除数
// 权重的更新
weights[rootQ] = weight * weights[p] / weights[q];
// 连通量-1
count--;
} // 判断p节点与q节点是否相连
public double connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if(rootP == rootQ){
return weights[q] / weights[p];
}else{
return -1.0;
}
} // 获取此时连通量的个数
public int getCount(){
return this.count;
} // 找x节点的父节点:同样具有路径压缩的!!!
private int find(int x) {
// 根节点的 parent[x] == x
if (parent[x] != x){
int origin = parent[x]; // x节点当前的父节点,用于权重的更新
parent[x] = find(parent[x]); // x节点直接指向根节点
weights[x] = weights[x] * weights[origin]; // 权重更新
}
// 返回根节点
return parent[x];
}
}

LeetCode:并查集的更多相关文章

  1. [leetcode] 并查集(Ⅰ)

    预备知识 并查集 (Union Set) 一种常见的应用是计算一个图中连通分量的个数.比如: a e / \ | b c f | | d g 上图的连通分量的个数为 2 . 并查集的主要思想是在每个连 ...

  2. [leetcode] 并查集(Ⅱ)

    最长连续序列 题目[128]:链接. 解题思路 节点本身的值作为节点的标号,两节点相邻,即允许合并(x, y)的条件为x == y+1 . 因为数组中可能会出现值为 -1 的节点,因此不能把 root ...

  3. [leetcode] 并查集(Ⅲ)

    婴儿名字 题目[Interview-1707]:典型并查集题目. 解题思路 首先对 names 这种傻 X 字符串结构进行预处理,转换为一个 map,key 是名字,val 是名字出现的次数. 然后是 ...

  4. Leetcode之并查集专题-765. 情侣牵手(Couples Holding Hands)

    Leetcode之并查集专题-765. 情侣牵手(Couples Holding Hands) N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手. 计算最少交换座位的次数,以便每对情侣可以并 ...

  5. Leetcode之并查集专题-684. 冗余连接(Redundant Connection)

    Leetcode之并查集专题-684. 冗余连接(Redundant Connection) 在本问题中, 树指的是一个连通且无环的无向图. 输入一个图,该图由一个有着N个节点 (节点值不重复1, 2 ...

  6. 【LeetCode】并查集 union-find(共16题)

    链接:https://leetcode.com/tag/union-find/ [128]Longest Consecutive Sequence  (2018年11月22日,开始解决hard题) 给 ...

  7. Leetcode题目200.岛屿数量(BFS+DFS+并查集-中等)

    题目描述: 给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量.一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的.你可以假设网格的四个边均被水包围. 示例 ...

  8. leetcode 76 dp& 强连通分量&并查集经典操作

    800. Similar RGB Color class Solution { int getn(int k){ return (k+8)/17; } string strd(int k){ char ...

  9. Java实现 LeetCode 721 账户合并(并查集)

    721. 账户合并 给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails ...

  10. C#LeetCode刷题-并查集

    并查集篇 # 题名 刷题 通过率 难度 128 最长连续序列   39.3% 困难 130 被围绕的区域   30.5% 中等 200 岛屿的个数   38.4% 中等 547 朋友圈   45.1% ...

随机推荐

  1. mysql多次连接后会产生最大失败值

    解决办法 可以更改max_connection_errors的值,即提高允许的max_connection_errors的数量 1.进入mysql 1)首先查看该属性设置为多大:命令:show glo ...

  2. 比年轻更年轻,快看能否接棒B站?

    撰文 |懂懂 编辑 | 秦言 来源:懂懂笔记 背靠超新Z世代,快看能否接棒B站? 国漫什么时候能追上日漫? 国漫作者真能挣到钱吗? 国漫什么时候才能走向世界? 这是中国漫画从业者的"灵魂三问 ...

  3. Apache网页优化

    目录: 一.Apache网页优化概述 二.网页压缩 三.网页缓存 四.隐藏版本信息 五.Apache防盗链 一.Apache网页优化概述 在企业中,部署Apache后只采用默认的配置参数,会引发网站很 ...

  4. v-for列表渲染之数组变动检测

    1.简单举一个v-for列表渲染例子 <template> <div> <ul> <li v-for="item in items"> ...

  5. FastDFS 配置 Nginx 模块及访问测试

    #备注:以下nginx-1.10.3源码目录根据nginx版本号不同会有相应的变化,以nginx版本号为准#一.安装 Nginx 和 fastdfs-nginx-module1,安装 Nginx 请看 ...

  6. nohup命令的用法

    在应用Unix/Linux时,我们一般想让某个程序在后台运行,于是我们将常会用 & 在程序结尾来让程序自动运行.比如我们要运行mysql在后台: /usr/local/mysql/bin/my ...

  7. pibbtimq高级使用技术,双向通信

    本案例目是是服务端发送客户端,客户端收到反应给服务端,双向通信客户端代码如下:import pikaimport timeconnection = pika.BlockingConnection(pi ...

  8. 【Python机器学习实战】决策树与集成学习(七)——集成学习(5)XGBoost实例及调参

    上一节对XGBoost算法的原理和过程进行了描述,XGBoost在算法优化方面主要在原损失函数中加入了正则项,同时将损失函数的二阶泰勒展开近似展开代替残差(事实上在GBDT中叶子结点的最优值求解也是使 ...

  9. springboot 事务创建流程源码分析

    springboot 事务创建流程源码分析 目录 springboot 事务创建流程源码分析 1. 自动加载配置 2. InfrastructureAdvisorAutoProxyCreator类 3 ...

  10. HDU 6170 Two strings( DP+字符串匹配)

    http://acm.hdu.edu.cn/showproblem.php?pid=6170 题目大意: 给出两个字符串s1和s2(长度小于等于2500). s1是一个正常的包含大小写字母的字符串,s ...