【算法】并查集

这篇博客介绍了并查集(Union-Find)数据结构在解决两类问题中的应用:一是130.被围绕的区域问题,通过并查集实现DFS的优化;二是990.等式方程的可满足性问题,通过并查集判断变量之间的等价关系。并查集的关键在于路径压缩和平衡性优化,确保高效操作。在被围绕的区域问题中,边界节点的O与特殊节点dummy连通,避免被替换;而在等式求解中,先将相等的变量连通,再判断不等式是否矛盾。

参考资料

关键术语解释:
连通分量:在一张含有多张图的网状结构中的,互相独立的图的个数。
union(q,p):将q和p连通起来,
find(p):返回p的根节点
connect(q,p):判断p和q是否连通
算法的关键点有 3 个:

1、用 parent 数组记录每个节点的父节点,相当于指向父节点的指针,所以 parent 数组内实际存储着一个森林(若干棵多叉树)。

2、用 size 数组记录着每棵树的重量,目的是让 union 后树依然拥有平衡性,而不会退化成链表,影响操作效率。

3、在 find 函数中进行路径压缩,保证任意树的高度保持在常数,使得 unionconnected API 时间复杂度为 O(1)。

class UF {
    // 连通分量个数,也就是独立的树的个数
    private int count;
    // 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
    private int[] parent;
    // 记录树的“重量”,平衡性优化
    private int[] size;

    public UF(int n) {
        this.count = n;
        parent = new int[n];
        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);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;
        
        // 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        count--;
    }

    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
    private int find(int x) {
        while (parent[x] != x) { 
            // 进行路径压缩,维持树的高度不超过3
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    public int count() {
        return count;
    }
}

例题应用

130. 被围绕的区域

130. 被围绕的区域

思路一
DFS

  1. 从边界开始遍历,进行dfs,将与边界的O相连的标记为#,记录为不标记
  2. 重新遍历数组,将#还原成O,将O标记为X
    代码
class Solution {
    public void solve(char[][] board) {
                int n = board.length;
        int m = board[0].length;
        for(int i=0;i<n;i++){
            dfs(board,i,0);
            dfs(board,i,m-1);
        }
        for(int j=0;j<m;j++){
            dfs(board,0,j);
            dfs(board,n-1,j);
        }
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(board[i][j]=='O'){
                    board[i][j]='X';
                }else if(board[i][j]=='#'){
                    board[i][j]='O';
                }
            }
        }
    }
    private void dfs(char[][] board,int i,int j){
        if(i<0||j<0||i>=board.length||j>=board[0].length||board[i][j]!='O'){
            return;
        }
        board[i][j]='#';
        dfs(board,i+1,j);
        dfs(board,i-1,j);
        dfs(board,i,j+1);
        dfs(board,i,j-1);
    }
    
}

思路二
并查集

  1. 边界节点的O不能被回收,我们首先将这些不能被回收的节点,利用并查集的connect连接起来。
  2. 将所有O节点连接起来,建立一颗颗树
  3. 最后遍历二维数组,如果这个O节点跟边界节点没有联通,就替换成X
    代码
class Solution {
void solve(char[][] board) {
    if (board.length == 0) return;

    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(n * (m - 1) + 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')
                // 将此 O 与上下左右的 O 连通
                for (int k = 0; k < 4; k++) {
                    int x = i + d[k][0];
                    int y = j + d[k][1];
                    if (board[x][y] == 'O')
                        uf.union(x * n + y, i * n + j);
                }
    // 所有不和 dummy 连通的 O,都要被替换
    for (int i = 1; i < m - 1; i++) 
        for (int j = 1; j < n - 1; j++) 
            if (board[i][j]=='O'&&!uf.connected(dummy, i * n + j))
                board[i][j] = 'X';
}
    
}
class UF {
    // 连通分量个数,也就是独立的树的个数
    private int count;
    // 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
    private int[] parent;
    // 记录树的“重量”,平衡性优化
    private int[] size;

    public UF(int n) {
        this.count = n;
        parent = new int[n];
        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);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;
        
        // 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        count--;
    }

    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
    private int find(int x) {
        while (parent[x] != x) { 
            // 进行路径压缩,维持树的高度不超过3
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    public int count() {
        return count;
    }
}

等式方程的可满足性

990.等式方程的可满足性

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

思路一

  1. 首先将a==b之类的等式单独拎出来,调用union方法连通,形成并查集。
  2. 然后对于所有不等式,调用connect方法判断是否连通,如果连通,说明不等式不成立。

代码

class Solution {
    public boolean equationsPossible(String[] equations) {
        UF uf = new UF(26);
        for(int i=0;i<equations.length;i++){
            if(!equations[i].contains("!")){
                char c1 = equations[i].charAt(0);
                char c2 = equations[i].charAt(equations[i].length()-1);
                uf.union(c1-'a',c2-'a');
            }
        }
        for(int i=0;i<equations.length;i++){
            if(equations[i].contains("!")){
                char c1 = equations[i].charAt(0);
                char c2 = equations[i].charAt(equations[i].length()-1);
                if(uf.connected(c1-'a',c2-'a')){
                    return false;
                }
            }
        }
        return true;
    }
}
class UF {
    // 连通分量个数,也就是独立的树的个数
    private int count;
    // 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
    private int[] parent;
    // 记录树的“重量”,平衡性优化
    private int[] size;

    public UF(int n) {
        this.count = n;
        parent = new int[n];
        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);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;
        
        // 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        count--;
    }

    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
    private int find(int x) {
        while (parent[x] != x) { 
            // 进行路径压缩,维持树的高度不超过3
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    public int count() {
        return count;
    }
}
克鲁斯卡尔算法是一种用于求解最小生成树的贪心算法,而并查集是一种用于维护元素分组信息的数据结构。它们在解决图论问题中经常一起使用。 克鲁斯卡尔算法的基本思想是,通过不断选择边权值最小且不会产生环路的边,逐步构建最小生成树。在实现过程中,使用并查集来判断两个节点是否属于同一个连通分量,以避免形成环路。 并查集是一种用于解决集合合并与查询问题的数据结构。它通过维护一棵树来表示每个元素所属的集合,其中每个节点指向其父节点,树的根节点表示该集合的代表元素。通过路径压缩和按秩合并等优化策略,可以提高并查集的效率。 在克鲁斯卡尔算法中,首先将图中的所有边按权值从小到大排序,然后依次选择边进行判断。当选择一条边时,判断该边连接的两个节点是否属于同一个连通分量。如果不属于同一个连通分量,则选择该边,并将两个节点合并到同一个连通分量中。重复这个过程直到选择了 n-1 条边,其中 n 是图中节点的个数,即得到最小生成树。 克鲁斯卡尔算法的时间复杂度主要取决于排序边的时间复杂度,一般情况下为 O(ElogE),其中 E 是边的数量。并查集的操作时间复杂度为 O(α(n)),其中 α(n) 是一个非常慢增长的函数,可以认为是常数级别。因此,整个算法的时间复杂度为 O(ElogE)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值