【笔记】并查集---无向图处理代码模板及类型题

在个人博客查看本文

并查集要处理的两个问题

  • 查询

查询图中两个顶点是否在同一个集合之中。

  • 合并

将两个不相交的集合进行合并。

设计并查集的两种思想

  • 基于id

给每个顶点分配一个唯一的标识,称为 id

id 不同则来自不同的集合。

合并时需要将其中一个集合中的所有元素的 id 赋值成为另一集合的 id 。需遍历整个一个集合的 id。

  • 基于parents

不再使用 id 来划分集合,而是根据他们的父节点进行集合划分。parent[i] 表示标识为 i 的结点的父亲结点的标识(可以形象地记为「找爸爸」)。在这个定义下,根结点的父亲结点是自己。这种方式形成的「并查集」组织成了 若干个不相交的树形结构,并且我们在访问结点的时候,总是按照「从下到上」进行访问的。

代表元法

「代表元」的「三个不重要」

  • 谁作为根结点不重要:根结点与非根结点只是位置不同,并没有附加的含义;

  • 树怎么形成的不重要:合并的时候任何一个集合的根结点指向另一个结合的根结点就可以;

  • 树的形态不重要:理由同「谁作为根结点不重要」。
    代表元法可能造成的问题

    树的高度过高,查询性能降低。

    解决方案有「按秩合并」与「路径压缩」。

按秩合并

按秩合并,一般指将秩较小的根结点指向秩较多的根节点。

这里的秩有两种含义:

  • size 合并
  • rank 合并

按 size 合并

让树的结点总数较小的树的根结点指向树的结点总数较大的树的根结点。

按 rank 合并

rank 合并的意思是让树的「高度」较小的树的根结点,指向树的「高度」较大的树的根结点。

之所以把「高度」称为 rank ,是因为同时使用「按秩合并」和「路径压缩」的时候,树的「高度」很难维护其准确的定义,但是依然可以作为合并时候的依据,因此称为「秩」。

路径压缩

隔代压缩

两步一跳,一直循环[把当前结点指向它的父亲结点的父亲结点]

完全压缩

把从 [查询结点] 到 [根结点] 沿途经过的所有结点都指向根结点。[完全压缩] 相比较于 [隔代压缩] 压缩的更彻底。

代码模板

class UnionFind {
    int[] parent;
    int n;

    public UnionFind(int n) {
        this.n = n;
        this.parent = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }
    
    public int findset(int x) {
        return parent[x] == x ? x : (parent[x] = findset(parent[x]));
    }
    
    public void unite(int x, int y) {
        x = findset(x);
        y = findset(y);
        if (x == y) {
            return;
        }
        parent[y] = x;

    }
}
 

并查集使用包括两种操作,查询和合并。

  • 使用 findset方法来实现并查集查询。parent[x] == x ? x : (parent[x] = findset(parent[x])) 寻找 x 的父结点。

如果 x 为根结点则有 parent[x] == x ,否则我们将继续寻找 x 父亲的父节点。即 parent[x] = findset(parent[x])

  • 受用 unite方法来实现集合的合并。首先要确定两个元素是否在同一集合中,使用findset查找其是否有共同父节点即可。
x = findset(index1);
y = findset(index2);
if (x == y) {
    return;
}

如果拥有共同父结点,则证明二者在一个集合中,无需合并。如果无公共父结点,我们只需要覆盖另外元素的父结点即可 parent[y] = x

基础问题

等式方程的可满足性

990. 等式方程的可满足性

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

示例 1:

输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。
示例 2:

输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:

输入:["a==b","b==c","a==c"]
输出:true
示例 4:

输入:["a==b","b!=c","c==a"]
输出:false
示例 5:

输入:["c==c","b==d","x!=z"]
输出:true

提示:

  1. 1 <= equations.length <= 500
  2. equations[i].length == 4
  3. equations[i][0]equations[i][3] 是小写字母
  4. equations[i][1] 要么是 '=',要么是 `'!'``
  5. ``equations[i][2]'='`

本题的重点在于如何将题目转化为并查集。由于存在有 a==b ,b==a,b!=c 这类关系,我们可将== 视为联通关系,而!=视为非联通关系,我们只需要根据已知条件建立联通关系以及非联通关系,确定其中是否存在有矛盾关系即可。

至于并查集的建立,因为存在有 a,b,c,d 等小写字母,且题目变量有且只有 小写字母与 =,!= 因此我们建立的并查集中 parent[] 的长度为26.

题目代码如下:

class Solution {
    public boolean equationsPossible(String[] equations) {
        UnionFind uf = new UnionFind(26);
        for(String str : equations){
            if(str.charAt(1)=='='){
                int index1 = str.charAt(0) - 'a';
                int index2 = str.charAt(3) - 'a';
                uf.unite(index1, index2);
            }
        }
        for(String str : equations){
            if(str.charAt(1)=='!'){
                int index1 = str.charAt(0) - 'a';
                int index2 = str.charAt(3) - 'a';
                if(uf.findset( index1) == 
                uf.findset(index2)){
                    return false;
                }
            }
        }
        return true;
    }
}
class UnionFind {
    int[] parent;
    int n;

    public UnionFind(int n) {
        this.n = n;
        this.parent = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }
    
    public int findset(int x) {
        return parent[x] == x ? x : (parent[x] = findset(parent[x]));
    }
    
    public void unite(int index1, int index2) {
        index1 = findset(index1);
        index2 = findset(index2);
        if (index1 == index2) {
            return;
        }
        parent[index2] = index1;

    }
}

省份数量

省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中isConnected[i][j] = 1表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例1:

【笔记】并查集---无向图处理代码模板及类型题

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

【笔记】并查集---无向图处理代码模板及类型题

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

提示:

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j] 为 1 或 0
  • isConnected[i][i]== 1
  • isConnected[i][j] == isConnected[j][i]

本题例子给出了图片,我们可以确定是图论问题,而这道题目主要考察的方向为连通问题,因此我们可以放心的使用并查集来解决问题。问题是共有几个省份。而省份是 一组直接或间接相连的城市,组内不含其他没有相连的城市。那么转化为并查集问题就是求解并查集中的集合共有几个。我们只需要求解有几棵树就可以了。

思路即为:查询当前节点的父结点。合并相同父结点的节点。确定树的个数。

代码如下:

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        UnionFind uf = new UnionFind(n);
        //查询以及合并集合
        for(int i = 0; i < n; i++){
            for(int j = i + 1; j < n; j++){
                if(isConnected[i][j]==1){
                    uf.unite(i, j);
                }
            }
        }
        //计算树的个数
        int cnt = 0;
        for(int i = 0; i < n; i++){
            if(uf.parent[i] == i){
                cnt++;
            }
        }
        return cnt;
    }
}
class UnionFind {
    int[] parent;
    int n;

    public UnionFind(int n) {
        this.n = n;
        this.parent = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }
    
    public int findset(int x) {
        return parent[x] == x ? x : (parent[x] = findset(parent[x]));
    }
    
    public void unite(int index1, int index2) {
        index1 = findset(index1);
        index2 = findset(index2);
        if (index1 == index2) {
            return;
        }
        parent[index2] = index1;

    }
}

冗余链接

684. 冗余连接

树可以看成是一个连通且 无环无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edgesedges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

示例 1:

【笔记】并查集---无向图处理代码模板及类型题

输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]

示例 2:

【笔记】并查集---无向图处理代码模板及类型题

输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]

提示:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= edges.length
  • ai != bi
  • edges 中无重复元素
  • 给定的图是连通的

非常常见的无向图联通问题。本题目的是为了让我们处理无向图成环的问题。即我们需要根据已知条件中的edges找出最后那条使得无向图成环的边。

考虑环是怎么形成的:

如果两个顶点属于不同的连通分量(即不同集合),则说明在遍历到当前的边之前,这两个顶点之间不连通,因此当前的边不会导致环出现,合并这两个顶点的连通分量。

如果两个顶点属于相同的连通分量(同一集合),则说明在遍历到当前的边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,将当前的边作为答案返回。

值得注意的是,我们的边是从1开始到n 因此在建立parent[]时,要将其长度设为 n+1 节点内容分别为 1~n

代码如下:

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        UnionFind uf = new UnionFind(n);
        for(int i = 0; i < n; i++){
            int[] edge = edges[i];
            int node1 = edge[0], node2 = edge[1];
            if(uf.findset(node1)!= uf.findset(node2)){
                uf.unite(node1, node2);
            }
            else{
                return edge;
            }
        }
        return new int[0];
    }
}
class UnionFind {
    int[] parent;

    public UnionFind(int n) {
        this.parent = new int[n+1];
        for (int i = 0; i <= n; ++i) {
            parent[i] = i;
        }
    }
    
    public int findset(int x) {
        return parent[x] == x ? x : (parent[x] = findset(parent[x]));

    }
    
    public void unite(int x, int y) {
        x = findset(x);
        y = findset(y);
        if (x == y) {
            return;
        }
        parent[y] = x;

    }
}
上一篇:国内 Java 开发者必备的两个装备,你配置上了么?


下一篇:1048. 最长字符串链