Skip to content

Day 48


108.冗余连接

并查集应用类题目,关键是如何把题意转化成并查集问题

题目链接:https://kamacoder.com/problempage.php?pid=1181

文章讲解:https://www.programmercarl.com/kamacoder/0108.冗余连接.html

视频讲解:https://www.bilibili.com/video/BV1gRM3z9EwZ

思路分析

使用并查集解决,逐次加入每一组边,如果发现新加入的边已经在集合中了,则说明是冗余边

题解

java
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int pointNUm  = scanner.nextInt();
        DisJoint disJoint = new DisJoint(pointNUm);
        for (int i = 0; i < pointNUm; i++) {
            disJoint.join(scanner.nextInt(), scanner.nextInt());
        }
    }
}

class DisJoint {
    private int[] father;

    public DisJoint(int N) {
        // N 个节点有 N - 1 条边
        father = new int[N + 1];
        // 初始时指向自己
        for (int i = 1; i < N; i++) {
            father[i] = i;
        }
    }

    public int find(int n) {
        // 找到了根节点,返回
        if (n == father[n]) {
            return n;
        }
        // 路径压缩
        return father[n] = find(father[n]);
    }

    public void join(int n, int m) {
        int rootN = find(n);
        int rootM = find(m);

        // 判断是否已经在同一个集合中,避免重复添加
        if (rootN == rootM) {
            // 如果已经在同一个集合中,输出(只输出一次)
            System.out.println(n + " " + m);
            return;
        }

        // 合并两个集合,按根节点合并
        father[rootN] = rootM;
    }

    public boolean isSame(int n, int m) {
        n = find(n);
        m = find(m);
        return m == n;
    }
}

⚠️ 109.冗余连接 II

上面两道题目是不是感觉做出自信了,感觉并查集不过如此? 来这道题目 给大家适当一些打击, 难度上来了。

题目链接:https://kamacoder.com/problempage.php?pid=1182

文章讲解:https://www.programmercarl.com/kamacoder/0109.冗余连接II.html

视频讲解:https://www.bilibili.com/video/BV1t2NEzaEMR

思路分析

本题从无向图转为了有向图,寻找冗余边的突破点就是寻找入度为 2 的节点

本题的本质是 :有一个有向图,由一颗有向树 + 一条冗余的有向边组成,需要找到那条冗余的边,把这条边删了,让这个图恢复为有向树

三大情况

(1)情况一:如果我们找到入度为 2 的点,那么删一条指向该节点的边就行了


(2)情况二:只能删特定的一条边


节点 3 的入度为 2,但在删除边的时候,只能删除由节点 1 -> 节点 3 的这条边,如果删除由节点 4 -> 节点 3 的这条边,此时就变成情况一了,成环了,删除边并不能达到恢复为有向树的目的

(3)情况三: 如果没有入度为 2 的点,说明图中有环了(注意是有向环)


对于情况三,删掉构成环的边就可以了

题解

java
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {

    static class Disjoint {
        private final int[] father;

        public Disjoint(int n) {
            father = new int[n + 1];
            for (int i = 1; i <= n; i++) {
                // 初始时指向自己
                father[i] = i;
            }
        }

        public void join(int n, int m) {
            // 首先判断是否在同一集合中
            m = find(m);
            n = find(n);
            if (m == n) {
                return;
            }
            father[n] = m;
        }

        public int find(int n) {
            // 找到了根节点
            if (n == father[n]) {
                return n;
            }
            return father[n] = find(father[n]);
        }

        public boolean isSame(int n, int m) {
            m = find(m);
            n = find(n);
            return m == n;
        }
    }

    static class Edge {
        int s;
        int t;

        public Edge(int s, int t) {
            this.s = s;
            this.t = t;
        }
    }

    static class Node {
        int in;
        int out;
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        List<Edge> edges = new ArrayList<>();
        // 节点编号从 1 开始,记录每一个节点的入度和出度
        Node[] nodeMap = new Node[n + 1];
        for (int i = 1; i <= n; i++) {
            nodeMap[i] = new Node();
        }
        Integer doubleIn = null;
        for (int i = 0; i < n; i++) {
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            // 统计入度
            nodeMap[t].in++;
            // 找到了入度大于 2 的节点,记录该节点
            if (!(nodeMap[t].in < 2)) {
                doubleIn = t;
            }
            // 加入边
            Edge edge = new Edge(s, t);
            edges.add(edge);
        }

        // 定义冗余边
        Edge result = null;

        // 存在入度为 2 的节点
        if (doubleIn != null) {
            // 记录两条入度边
            List<Edge> doubleInEdges = new ArrayList<>();
            for (Edge edge : edges) {
                // 遍历所有边,找到入度为 2 的节点的两条入度边
                if (edge.t == doubleIn) {
                    doubleInEdges.add(edge);
                }
            }

            // 找到两条入度边后,优先处理后面那一条边
            Edge edge = doubleInEdges.get(1);

            // 情况一:存在入度为 2 的节点,删除该边后是有向树
            if (isTreeWithExclude(edges, nodeMap, edge)) {
                result = edge;
            }else { // 情况二:存在入度为 2 的节点,删除该边后,无法形成有向树,则删除前面那条边
                result = doubleInEdges.get(0);
            }
        } else {
            // 情况三:不存在入度为 2 的节点,解除环即可(用并查集找冗余的边)
            result = getRemoveEdge(edges, nodeMap);
        }
        // 通过边找到链接的两个节点
        System.out.println(result.s + " " + result.t);
    }

    public static boolean isTreeWithExclude(List<Edge> edges, Node[] nodeMap, Edge excludeEdge) {
        Disjoint disjoint = new Disjoint(nodeMap.length);
        for (Edge edge : edges){
            // 如果遍历到需要删除的边,则不做处理,交给删除边的方法处理
            if (edge == excludeEdge){
                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;
        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;
    }
}