并查集理论基础

并查集用来解决连通性问题

连通图 + 强连通图

在无向图中,任何两个节点都是可以到达的,我们称之为连通图;如果有节点不能到达其他节点,则为非连通图。

在有向图中,任何两个节点都是可以到达的,我们称之为强连通图。与无向图中的连通图有些不同,因为强连通图是有向图作为基础的,必须任何两个节点都是可以相互到达的,所以要么两节点是双向连接的,要么整个图成环。

何为解决连通性问题?通俗理解就是判断两个元素是否在同一个集合里。

并查集主要有两个功能:

功能一:将两个(或多个)元素添加到一个集合中;

功能二:判断两个(或多个)元素在不在同一个集合。

先来看功能一如何实现:将两个(或多个)元素添加到一个集合中。

我们将三个元素A,B,C(分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢?

只需要用一个一维数组来表示,即:father[A] = B,(表示A指向B),father[B] = C,(表示B指向C),这样一来就表示A与B与C连通了(有向连通图)。

代码如下:

//将节点 v 指向 节点 u 的这条边 加入 并查集
void join(int u, int v){
   u = find(u);//找节点u的根节点,并赋值为u
   v = find(v);//找节点v的根节点,并赋值为v
   if(u == v) return;//如果节点u和节点v的根节点是同一个,则说明在同一个集合中,不用两个节点相连直接返回即可
   father[v] = u;//说明两个节点根节点不是一个,那么就把两个节点相连,节点v指向节点u
}

至此,通过join方法实现了功能一。我们可以知道A连通B,因为A是索引下标,根据father[A]的数值就可以知道A连通B。那么怎么知道B连通A呢?

因为我们的目的是实现功能二:判断多个元素是否在同一个集合中,所以知道A连通B就已经足够了。

寻根思路就是A,B,C在同一个根下就是同一个集合。

具体来说:给出A元素,就可以通过father[A] = B,father[B] = C,找到根为C

给出B元素,通过father[B] = C,找到根也为C。说明A和B是在同一个集合中。

代码如下:

//并查集寻根的过程
int find(int u){
    if(u == father[u]) return u;//如果根就是自己,直接返回
    else return find(father[u]); //如果根不是自己,就根据其父节点不断递归找到根节点
}

如何表示C也在同一个元素里呢?我们需要father[C] = C,即C的根也为C,这样就方便表示A,B,C都在同一个集合中了。

所以father数组初始化的时候要father[i] = i,默认自己指向自己

代码如下:

// 并查集初始化
void init(){
    for(int i = 0; i < n; i++){
      father[i] = i;
    }
}
最后再来看看如何实现功能二:判断两个(或多个)元素在不在同一个集合。

如果通过 find 函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合

代码如下:

// 判断 u 和 v 是否找到同一个根
boolean isSame(int u, int v){
    u = find(u);
    v = find(v);
    return u == v;
}

路径压缩:

在实现find函数的过程中,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。如果递归次数非常多会导致效率低。我们的目的只需要知道这些节点在同一个根下就可以,所以除了根节点其他所有节点都挂在根节点下,这样在寻根的时候就很快,只需要一步,由原来的时间复杂度O(logn)降为O(1)。

想要实现这样的效果,需要路径压缩,将非根节点的所有节点直接指向根节点。

只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。

因为find 函数 向上寻找根节点,father[u]表述 u 的父节点,那么让 father[u] 直接获取 find 函数 返回的根节点,这样就让节点 u 的 父节点 变成 根节点。

代码如下:

//并查集寻根的过程
int find(int u){
    return u == father[u] ? u : father[u] = find(father[u]);  
}

综上:并查集主要有三个功能:

1.寻找根节点,函数:find(int u),也就是判断节点u的根节点是哪个;

2.将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点下;

3.判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点。

模板代码总结如下:

// 并查集初始化
void init(){
    for(int i = 0; i < n; i++){
      father[i] = i;
    }
}
//将节点 v 指向 节点 u 的这条边 加入 并查集
void join(int u, int v){
    u = find(u);//找节点u的根节点,并赋值为u
    v = find(v);//找节点v的根节点,并赋值为v
    if(u == v) return;//如果节点u和节点v的根节点是同一个,则说明在同一个集合中,不用两个节点相连直接返回即可
    father[v] = u;//说明两个节点根节点不是一个,那么就把两个节点相连,节点v指向节点u
}
//并查集寻根的过程
int find(int u){
    return u == father[u] ? u : father[u] = find(father[u]);  
}
// 判断 u 和 v 是否找到同一个根
boolean isSame(int u, int v){
    u = find(u);
    v = find(v);
    return u == v;
}

107.寻找存在的路径

通过并查集将图转成树的形式。判断两个节点在不在一个集合(就是判断两个节点是不是拥有同一个根节点)+将两个节点添加到一个集合中(就是将这两个节点都指向同一个根节点)。

代码如下:

import java.util.*;

//并查集
public class Main{
    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        DisJoint disJoint = new DisJoint(n);
        for(int i = 0; i < m; i++){
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            disJoint.join(s, t);
        }
        int source = scanner.nextInt();
        int destination = scanner.nextInt();
        if(disJoint.isSame(source, destination) == true){
            System.out.print(1);
        }else{
            System.out.print(0);
        }
    }  
}
//并查集
class DisJoint{
    private int[] father;
    public DisJoint(int n){
        father = new int[n + 1];
        for(int i = 0; i <= n; i++){
            father[i] = i;
        }
    }
    public void join(int u, int v){
        u = find(u);
        v = find(v);
        if(u == v){
            return;
        }
        father[v] = u;
    }
    
    public int find(int u){
        return u == father[u] ? u : (father[u] = find(father[u]));
    }
    public boolean isSame(int u, int v){
        u = find(u);
        v = find(v);
        return u == v;
    }
}

108.冗余连接

解题思路:

从前向后遍历每一条边(优先让前面的边连上),边的两个节点如果不在同一个集合(isSame(u, v) == false),那就加入集合(join(u, v))(即同一个根节点)。如果边的两个节点已经出现同一个集合中(isSame(u, v) == true),说明这条边的两个节点已经连在一起了,再加入这条边就一定会成环。

代码如下:

import java.util.*;
//思路 :如果两个节点 是 都指向同一个根节点(即是在同一个集合中),那么再将这两个节点
//连起来,就一定构成环
public class Main{
    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        DisJoint disJoint = new DisJoint(n);
        for(int i = 0; i < n; i++){
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            if(disJoint.isSame(s, t) == false){//说明两节点不在同一个集合
                //加入同一个集合
                disJoint.join(s, t);
            }else{//说明两节点在同一个集合
                System.out.print(s + " " + t);
            }
        }      
    }    
}

class DisJoint{
    private int[] father;
    public DisJoint(int n){
        father = new int[n + 1];
        for(int i = 0; i <= n; i++){
            father[i] = i;
        }
    }
    public void join(int u, int v){
        u = find(u);
        v = find(v);
        if(u == v){
            return;
        }
        father[v] = u;
    }
    public int find(int u){
        return u == father[u] ? u : (father[u] = find(father[u]));
    }
    public boolean isSame(int u, int v){
        u = find(u);
        v = find(v);
        return u == v;
    }  
}

109.冗余连接II

有向树:该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都 有且只有 一个父节点,而根节点没有父节点。有向树拥有n个节点和n-1条边。

现在有一个有向图,有向图是在 有向树 中的两个没有直接连接 的 节点 中间添加一条有向边。所以有向图是有 n 个节点 和 n条边组成的。

现在需要返回一条可以删除的边,使得删除这条边之后 该有向图 变成了 一颗 有向树

解题思路:

有向树的性质是:只有根节点入度为0,其他节点入度都为1。

情况一:找到入度为2的点,那么删除一条指向该节点的边即可。

情况二:找到入度为2的点,但是只能删除特定的一条边。

情况三:如果没有入度为2的点,说明 图中 有环了,就和 冗余连接 那道题一样了,删除构成环的边即可。

具体写法步骤:

1、把每条边记录下来,并统计节点入度,这道题只会存在一个入度为2的点

2、前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条。如果删了一条,就判断这个图是不是一颗有向树,如果是,那么这条边就是答案;如果不是,就删除另外那条边。同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为有向树,就删最后那一条。

3、第三种情况3,明确没有入度为2的情况下,那么一定有 有向环, 找到构成环 的 边就是要删除的边。

综上 我们 需要构建两个方法:

方法一:isTreeAfterRemoveEdge() 判断删一个边之后是不是有向树:将所有边的两端节点分别加入并查集,遇到要删除的边则跳过;如果遇到即将加入并查集的边的两端节点 本来就在并查集了,则说明构成了环。如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除 该条边 还是一棵有向树。

方法二:getRemoveEdge() 确定图中 一定 有了 有向环,那么要找到需要删除的那条边:将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点本来就在并查集了,说明构成了环。

代码如下:

import java.util.*;

public class Main{
    static class Disjoint {

        private final int[] father;

        public Disjoint(int n) {
            father = new int[n + 1];
            for (int i = 0; i <= n; i++) {
                father[i] = i;
            }
        }

        public void join(int n, int m) {
            n = find(n);
            m = find(m);
            if (n == m) return;
            father[n] = m;
        }

        public int find(int n) {
            return father[n] == n ? n : (father[n] = find(father[n]));
        }

        public boolean isSame(int n, int m) {
            return find(n) == find(m);
        }
    }
    
    static class Edge{
        int s;
        int t;
        public Edge(int s, int t){
            this.s = s;
            this.t = t;
        }
    }
    static class Node{
        int id;
        int in;
        int out;
    }
    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        //记录入度为2的节点是哪一个(本题只能有一个入度为2的点)
        //装填所有输入的节点以及对应的边信息
        List<Edge> edges = new ArrayList<>();
        Node[] nodeMap = new Node[n + 1];
        for (int i = 1; i <= n; i++) {
            nodeMap[i] = new Node();
        }
        int doubleIn = 0;
        for(int i = 0; i < n; i++){
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            nodeMap[t].in++;
            if(nodeMap[t].in == 2){
                doubleIn = t;
            }
            edges.add(new Edge(s, t));
        }
        //开始走三个情况的逻辑
        Edge result = null;
        if(doubleIn != 0){//说明有入度为2的节点
            List<Edge> doubleInEdges = new ArrayList<>();
            for(Edge edge : edges){
                if(edge.t == doubleIn){
                    doubleInEdges.add(edge);
                }
                if(doubleInEdges.size() == 2){
                    break;
                }
            }
            Edge edge = doubleInEdges.get(1);
            if (isTreeAfterRemoveEdge(edges, edge, nodeMap)) {
                result = edge;
            } else {
                result = doubleInEdges.get(0);
            }
        }else{//说明没有入度为2的节点
            result = getRemoveEdge(edges, nodeMap);
        }
        System.out.println(result.s + " " + result.t);
    }
    public static boolean isTreeAfterRemoveEdge(List<Edge> edges, Edge exculdEdge, Node[] nodeMap) {
        Disjoint disjoint = new Disjoint(nodeMap.length - 1);
        for (Edge edge : edges) {
            if (edge == exculdEdge) continue;
            //成环则不是树
            if (disjoint.isSame(edge.s, edge.t)) {
                return false;
            }
            disjoint.join(edge.s, edge.t);
        }
        return true;
    }

    public static Edge getRemoveEdge(List<Edge> edges, Node[] nodeMap) {
        int length = nodeMap.length - 1;
        Disjoint disjoint = new Disjoint(length);

        for (Edge edge : edges) {
            if (disjoint.isSame(edge.s, edge.t)) return edge;
            disjoint.join(edge.s, edge.t);
        }
        return null;
    }  
}

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注