每日算法总结——拓扑排序算法、求最小生成树算法(Kruskal、Prim)、单源最短路径算法(Dijkstra)

一、拓扑排序算法

  • 适用范围:要求有向图,且有入度为0的节点,且没有环。

  • 算法思想:记录每个节点的入度,并且该入度可以随着节点的删除而变化,因此使用HashMap记录每个点当前的入度,并使用队列来依次存储每个入度为0的节点,并依次处理。

public class TopologySort {
    public static List<Node> sortTopology(Graph graph) {
        // Key: 某一个node, Value: 该node当前的入度
        Map<Node, Integer> inMap = new HashMap<>();
        // 入度为0的点,才能进这个队列
        Queue<Node> zeroInQueue = new LinkedList<>();
        // 首先需要把图中所有点的入度信息放入inMap中,同时找出入度为0的点并放入队列
        for (Node node : graph.nodes.values()) {
            inMap.put(node, node.in);
            if (node.in == 0) {
                zeroInQueue.add(node);
            }
        }
        List<Node> result = new ArrayList<>();
        while (!zeroInQueue.isEmpty()) {
            Node cur = zeroInQueue.poll();
            result.add(cur);
            for (Node next : cur.nexts) {
                inMap.put(next, inMap.get(next) - 1);
                if (inMap.get(next) == 0) {
                    zeroInQueue.add(next);
                }
            }
        }
        return result;
    }
}
实战1:课程表
实战2:课程表 II

二、求最小生成树算法

  • 生成树的定义:一个连通图的生成树是一个极小的连通子图,它包含图中全部的n个顶点,但只有构成一棵树的n-1条边。

  • 最小生成树的定义:所谓一个带权图的最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和

Kruskal算法(K算法)

算法思想:每次都选择权值最小的边,如果向生成树添加该边后不产生环路,则添加该边,否则不添加该边,…,重复操作直到所有的节点都连通。

  • 最小边的选择最小堆(优先队列)
  • 如何判断是否有环:利用并查集,初始时各个节点单独为一个集合,每次添加一条边,判断两端点所在集合是否为同一个集合,若为同一个集合,则表示会产生环,因此不添加该边;不是同一个集合就把该边两端点所在的集合合并。
public class Kruskal {
    public static class EdgeComparator implements Comparator<Edge> {

        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }
    }

    public static Set<Edge> kruskalMst(Graph graph) {
        UnionFind unionFind = new UnionFind();
        // 初始时每个点单独为一个集合
        unionFind.makeSet(graph.nodes.values());
        Queue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        for (Edge edge : graph.edges) {
            // M条边,O(logM)
            priorityQueue.add(edge);
        }
        Set<Edge> result = new HashSet<>();
        while (!priorityQueue.isEmpty()) {
            Edge edge = priorityQueue.poll();
            if (!unionFind.isSameSet(edge.from, edge.to)) {
                unionFind.union(edge.from, edge.to);
                result.add(edge);
            }
        }
        return result;
    }
}
扩展——并查集详解
  • 什么是并查集?

    • 并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题,关键操作如下:
      1. Find确定元素属于哪个集合
        • 并查集中每棵树都表示一个单独的集合,树的根节点代表了整个集合,判断元素属于哪个集合,只需要向上查找它的根节点,根节点可以用来确定两个元素是否属于同一集合。
      2. Union将两个不相关的集合合并为一个集合
  • 并查集的基本思路就是围绕根节点展开,但是如果每次合并时都只是让一棵树的根节点挂到另一棵树的根节点上的话,会存在树退化成链的情况,所以需要进行优化,即路径压缩

    • 其实就是一个推理的过程:如果A是B的老师,B是C的老师,则很容易就得到A是C的老师。
    • 可以在Find操作过程中进行路径压缩。
  • 代码模版如下:

    static class UnionFind {
        /**
         * parent[i]为节点i的父节点
         * size[i]表示以i为根节点的集合中元素的个数
         */
        int[] parent;
        int[] size;
    
        public UnionFind(int n) {
            parent = new int[n + 1];
            size = new int[n + 1];
            int index = 1;
            Arrays.fill(size, 1);
            for (int i = 0; i < parent.length; i++) {
                parent[i] = i;
            }
        }
    
        public int find(int o) {
            // 如果parent[o]不是根节点,就将找到的根节点赋值给parent[o]
            return parent[o] == o ? o : (parent[o] = find(parent[o]));
        }
    
        public Boolean isSameSet(int o1, int o2) {
            return find(o1) == find(o2);
        }
    
        public void union(int o1, int o2) {
            o1 = find(o1);
            o2 = find(o2);
            if (o1 == o2) {
                return;
            }
            // 窄树往宽树上挂, 可以减少「路径压缩」的次数
            if (size[o1] < size[o2]) {
                o1 = o1 ^ o2;
                o2 = o1 ^ o2;
                o1 = o1 ^ o2;
            }
            parent[o2] = o1;
            size[o1] += size[o2];
        }
    }
    
实战1:冗余连接
实战2:冗余连接II
  • leetcode 原题:685. 冗余连接 II - 力扣(LeetCode)

  • 难度等级:Hard

  • 解题思路:

    • 这道题十分滴抽象😵,需要进行情况讨论,在这里写一下我的总结
    • 由于多了边的方向性,所以不能仅仅利用并查集判断环的存在,还要判断冲突(即i存在入度为2的节点),比如下面的情况:
      [[1, 2], [2, 3], [3, 1], [4, 2]]
      按照实战1的解法,即利用并查集判断是否成环的方法,在循环走到[3, 1]这条边时就需要返回,但实际上应该返回[1, 2],因为[1, 2]、[4, 2]这两条边会使节点2有两个父节点。
    • 此时,需要对有没有环,是否冲突同时进行考虑:
      1. 存在有环边,但不存在入度为2的顶点image.png
      2. 存在入度为2的顶点,但不存在有环边image.png
      3. 同时存在有环边和入度为2的顶点image.png
    • 对于存在入度为 2 顶点的情况,一定是删除指向入度为 2 的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案。
    • 对于没有入度为 2 顶点的情况,那么一定有有向环,找到构成环的边就是要删除的边。
  • 官方的解是经过优化后的,不是很好想🤯。

    • 需要明确一点:有向图中的环必须是有向环,但并查集判断的环是所有无向环。
      • 冲突没有发生时,一定有成环的边,此时返回成环的边即可。
      • 当冲突发生时,记这条边为[u, v],则有两条边指向节点v,另一条边为[parent[v], v]
        • 如果之前或之后没有出现过环,那么附加的边一定是[u, v](因为如果此时既产生冲突又产生环,罪魁祸首的边一定就是这个边)
        • 如果之前或之后出现过环,则附加的边一定是[parent[v], v],因为即使删除了[u, v],原本存在的环,还是存在;所以一定得删除[parent[v], v]
    class Solution {
        static class UnionFind {
            /**
             * parent[i]为节点i的父节点
             * size[i]表示以i为根节点的集合中元素的个数
             */
            int[] parent;
            int[] size;
    
            public UnionFind(int n) {
                parent = new int[n + 1];
                size = new int[n + 1];
                int index = 1;
                Arrays.fill(size, 1);
                for (int i = 0; i < parent.length; i++) {
                    parent[i] = i;
                }
            }
    
            public int find(int o) {
                return parent[o] == o ? o : (parent[o] = find(parent[o]));
            }
    
            public Boolean isSameSet(int o1, int o2) {
                return find(o1) == find(o2);
            }
    
            public void union(int o1, int o2) {
                o1 = find(o1);
                o2 = find(o2);
                if (o1 == o2) {
                    return;
                }
                // 小树往大树上挂
                if (size[o1] < size[o2]) {
                    o1 = o1 ^ o2;
                    o2 = o1 ^ o2;
                    o1 = o1 ^ o2;
                }
                parent[o2] = o1;
                size[o1] += size[o2];
            }
        }
        public int[] findRedundantDirectedConnection(int[][] edges) {
            UnionFind unionFind = new UnionFind(edges.length);
            int[] parent = new int[edges.length + 1];
            Arrays.fill(parent, 0);
            // 记录产生冲突和环时的边
            int cycle = -1;
            int infliction = -1;
            for (int i = 0;i < edges.length; ++i) {
                if (parent[edges[i][1]] != 0) {
                    infliction = i;
                } else {
                    parent[edges[i][1]] = edges[i][0];
                    if (unionFind.isSameSet(edges[i][0], edges[i][1])) {
                        cycle = i;
                    } else {
                        unionFind.union(edges[i][0], edges[i][1]);
                    }
                }
            }
            if (infliction == -1) {
                return edges[cycle];
            } else {
                if (cycle == -1) {
                    return edges[infliction];
                } else {
                    return new int[]{parent[edges[infliction][1]], edges[infliction][1]};
                }
            }
        }
    }
    
Prim算法(P算法)
  • 算法思路
    • 初始时准备一个点集(用于标记已经连通的点)、一个优先级队列
    • 选择一个点(任意)加入点集中,获取这个点集中的点连接的所有边并加入优先级队列;
    • 从优先级队列中取出一个权值最小的边,如果该边的另一端点(to端点)不在点集中,则将其加入点集;否则丢弃该边,再次从优先级队列中取出权值最小的边。
    • 加入新点后,将该点所有连接的边加入优先级队列,重复上述操作,直到优先级队列为空。
  • 注意考虑图不连通的情况,此时需要求不同连通区域各自的最小生成树,下面代码中的for循环就是这个作用。
public class Prim {
    public static class EdgeComparator implements Comparator<Edge> {

        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }
    }
    public static Set<Edge> primMst(Graph graph) {
        // 解锁的边进入小根堆
        Queue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        HashSet<Node> set = new HashSet<>();
        Set<Edge> result = new HashSet<>();
        /*
        for循环的作用:解决图不连通的情况
        当有多个连通区域时,分别生成各个连通区域的最小生成树(森林)
         */
        for (Node node : graph.nodes.values()) {
            // node是开始点
            if (!set.contains(node)) {
                set.add(node);
                // 由一个点解锁所有相连的边
                for (Edge edge : node.edges) {
                    priorityQueue.add(edge);
                }
                while (!priorityQueue.isEmpty()) {
                    // 弹出解锁的边中,最小的边
                    Edge edge = priorityQueue.poll();
                    // edge.to 可能的一个新点
                    if (!set.contains(edge.to)) {
                        result.add(edge);
                        set.add(edge.to);
                        for (Edge nextEdge : edge.to.edges) {
                            priorityQueue.add(nextEdge);
                        }
                    }
                }
            }
        }
        return result;
    }
}

三、单源最短路径算法——Dijkstra算法

  • 适用范围:没有权值为负的边。

  • 算法思想

    1. 通过Dijkstra计算图G中的最短路径时,需要指定一个起点D (即从顶点D开始计算)。
    2. 此外,引进一个HashSetS和一个HashMap<Node, Integer>US的作用是记录已求出最短路径的顶点,而U则是记录起点D到图中各个节点的距离。
    3. 初始时,U中只有<起点D,0>,其他节点暂时没有放入U代表起点到这些点的距离为正无穷。
    4. 然后,从U中找出路径最短的顶点K,并将其加入到集合S中;同时,根据连接K各条边的距离更新U中的各顶点到起点D的距离(和原值比较,取最小值)。
    5. 重复第4步操作,直到遍历完所有顶点。
public class Dijkstra {
    /**
     * 求取head到其他各点的最短路径
     * @param head 源点
     * @return 源点到其他各个点的最短路径, key: 从head出发到达key; value: 从head出发到达key的最小距离
     */
    public static HashMap<Node, Integer> dijkstra(Node head) {
        // 如果在表中,没有T的记录,含义是从head出发到T这个点的距离为正无穷
        HashMap<Node, Integer> distanceMap = new HashMap<>();
        distanceMap.put(head, 0);
        // 已经求过距离的节点,放入selectNodes中,以后再也不碰
        HashSet<Node> selectedNodes = new HashSet<>();
        Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
        while (minNode != null) {
            int distance = distanceMap.get(minNode);
            for (Edge edge : minNode.edges) {
                Node toNode = edge.to;
                if (!distanceMap.containsKey(toNode)) {
                    distanceMap.put(toNode, distance + edge.weight);
                } else {
                    distanceMap.put(toNode, Math.min(distance + edge.weight, distanceMap.get(toNode)));
                }
            }
            selectedNodes.add(minNode);
            minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
        }
        return distanceMap;
    }

    /**
     * 获取distanceMap中距离head最短且不在selectNodes中的点
     * @param distanceMap 当前head距离各个点的距离,没有表示负无穷(不考虑)
     * @param selectedNodes 已经锁定了的节点,就是指这些点已经找到了最段距离
     * @return distanceMap中距离head最短且不在selectNodes中的点
     */
    public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap,
                                                       HashSet<Node> selectedNodes){
        Node minNode = null;
        int minDistance = Integer.MAX_VALUE;
        for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {
            Node cur = entry.getKey();
            int distance = entry.getValue();
            if (!selectedNodes.contains(cur) && distance < minDistance) {
                minDistance = distance;
                minNode = cur;
            }
        }
        return minNode;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值