0
点赞
收藏
分享

微信扫一扫

算法25:图相关算法

一叶随风_c94d 2023-05-18 阅读 113
图论

图的基础概念我就不去介绍了,不懂的可以去看看相关技术类书籍,讲的云里雾里的,很难懂。到最后发现,全是一堆屁话,核心还是要去了解图的算法。

我自己概括一下:

1、图就是一堆顶点和边的集合,也可以把图抽象成一个具体的类,而这个类里面有一堆顶点和边的集合。

2. 而这些边,是有方向的,开始节点,结束节点。

3. 顶点,就是边中开始节点或者结束节点,而这些节点需要持有边,只有这样,我们才能知道它有哪些边。

图的表示,有很多的表示方法。像什么邻接矩阵、邻接表、十字链表等等,各种各样奇葩的表示都有。算法并不一定难,但是这么多表示图的结构中,每一种结构都要掌握对应的算法,这个就有点烧脑了。

综上所述,我自己定义了一个适合自己版本的图的表示方法。不管你给我什么结构表示的图,我都用我自己熟悉的图的渲染方式,通过一个适配类转换成自己的图。在自己熟悉的结构中,根据实际的业务去修改自己熟悉的代码,相对而言会简单许多。

下面来看看我自己设计的图的结构.

首先定义图Graph结构:

package code03.图_05.结构;

import java.util.HashMap;
import java.util.HashSet;

public class Graph {

    public HashMap<Integer, Node> nodes;
    public HashSet<Edge> edges;

    public Graph() {
        nodes = new HashMap<>();
        edges = new HashSet<>();
    }
}

再定义边结构:

package code03.图_05.结构;

public class Edge {

    public int weight;  //权重
    public Node from;   //开始顶点
    public Node to;     //结束顶点

    public Edge(int w, Node f, Node t) {
        weight = w;
        from = f;
        to = t;
    }
}

点结构:

package code03.图_05.结构;


import java.util.ArrayList;

public class Node {

    public int val;     //当前点对应的值
    public int in;      //有多少条边指向这个顶点,称为入度。根据业务定
    public int out;     //这个点作为开始顶点,一个出去多少边,称为出度。根据业务定

    //当前节点出去找到的节点
    public ArrayList<Node> neighborNodes;
    //当前节点出去的边
    public ArrayList<Edge> neighborEdges;

    Node (int value) {
        val = value;
        in = 0;
        out = 0;
        neighborNodes = new ArrayList<>();
        neighborEdges = new ArrayList<>();
    }
}

有了以上3个实现类,现在还差一个适配类。适配类是需要根据自己面对的具体业务逻辑去写的。

下面以数组形式为例。

假设,现在给你一堆数组,[[3,0,3],[5,3,6].........]. 这代表什么意思呢?解释一下,二维数组中,每一个子数组有3个值,第一个代表边的权重,第二个代表边的开始节点,第三个代表边的结束节点。【5,3,6】 就代表从顶点 3 到顶点6的边,而边的权重为5。

代码如下:

package code03.图_05.结构;

public class GenerateGrap {

    // matrix 所有的边
    // N*3 的矩阵
    // [weight, from节点上面的值,to节点上面的值]
    //
    // [ 5 , 0 , 7]
    // [ 3 , 0,  1]

    public static Graph createGraph(int[][] matrix)
    {
        Graph graph = new Graph();
        for (int i = 0; i < matrix.length; i++)
        {
            int weight = matrix[i][0];
            int from = matrix[i][1];
            int to = matrix[i][2];

            //点搜集
            if (!graph.nodes.containsKey(from)) {
                graph.nodes.put(from, new Node(from));
            }
            if (!graph.nodes.containsKey(to)) {
                graph.nodes.put(to, new Node(to));
            }

            Node fromNode = graph.nodes.get(from);
            Node toNode = graph.nodes.get(to);
            Edge edge = new Edge(weight, fromNode, toNode);
            //边搜集
            if (!graph.edges.contains(edge)) {
                graph.edges.add(edge);
            }

            fromNode.out++;
            fromNode.neighborEdges.add(edge);
            fromNode.neighborNodes.add(toNode);
            toNode.in++;
        }
        return graph;
    }

    /**                     5
     *              2   ---------->   8
     *           3/     4        2/     \4
     *          1   ------>    3           7
     *            \               7   /
     *            2\                /
     *                   4
     */
    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{7,4,7},{4,8,7}};
        Graph graph = createGraph(matrix);
        System.out.println("111");
    }
}

其实,此时,一个图就构造好了。如果遇到的是二维数组,就可以写出这样的适配类来进行转化。当然,还有很多的表示方法,需要根据不同的表示方法,设计出不同的适配类。反正,图的结构就是这样。也可以根据实际的业务,去设计整个结构,使图有不同的结构。

图的广度优先遍历(Breadth First Search)

我们接触二叉树的时候,讲过树的层序遍历。其实,图的广度优先遍历和二叉树的层序优先遍历逻辑一样。就是找到一个顶点,然后把它的子节点给全部遍历出来,然后根据子节点再找孙子节点,再把每个子节点的孙子节点给遍历出来,依次类推。

package code03.图_05;

import code03.图_05.结构.GenerateGrap;
import code03.图_05.结构.Graph;
import code03.图_05.结构.Node;

import java.util.*;

public class BreadthFirstSearch_01 {

    //广度优先遍历
    public static void bfs (Graph graph)
    {
        //边界值判断
        if (graph == null) {
            return;
        }
        Queue queue = new LinkedList();
        Set set = new HashSet();

        HashMap<Integer, Node> nodes = graph.nodes;
        //锁定顶点,就是入度为0的节点
        for(Iterator iterator = nodes.keySet().iterator(); iterator.hasNext();) {
            int key = (int) iterator.next();
            Node val = nodes.get(key);
            if (val.in == 0) {
                queue.add(val);
                set.add(val);
                break;
            }
        }

        //队列,先进先出,这是层序遍历的核心
        while (!queue.isEmpty())
        {
            Node cur = (Node) queue.poll();
            System.out.println("node : " + cur.val);

            //遍历所有子节点
            for(int i = 0; i < cur.neighborNodes.size(); i++) {
                Node next = cur.neighborNodes.get(i);
                //如果set中有值,说明这个子节点已经被遍历过了,无需再次遍历
                if (!set.contains(next)) {
                    set.add(next);
                    queue.add(next);
                }
            }
        }
    }


    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{7,4,7},{4,8,7}};
        //使用我们自己设计的图结构
        Graph graph = GenerateGrap.createGraph(matrix);
        bfs(graph);
    }
}

图的深度优先遍历(Depth First Search)

深度优先遍历,就是根据顶点锁定一个子节点,一直遍历到最后。 然后返回,锁定另一个子节点,一直遍历到最后,依次类推。也就是说,一旦锁定一个子节点,会把这个子节点下方所有的节点都给遍历完为止。

package code03.图_05;

import code03.图_05.结构.GenerateGrap;
import code03.图_05.结构.Graph;
import code03.图_05.结构.Node;

import java.util.*;

public class DepthFirstSearch_02 {

    //深度优先遍历
    public static void dfs (Graph graph)
    {
        if (graph == null) {
            return;
        }
        Stack stack = new Stack();
        Set set = new HashSet();

        HashMap<Integer, Node> nodes = graph.nodes;
        //锁定一个顶点
        for(Iterator iterator = nodes.keySet().iterator(); iterator.hasNext();) {
            int key = (int) iterator.next();
            Node val = nodes.get(key);
            if (val.in == 0) {
                stack.push(val);
                set.add(val);
                break;
            }
        }

        //栈、后进先出,这是深入优先遍历的核心
        while (!stack.isEmpty())
        {
            Node cur = (Node) stack.pop();
            System.out.println("node : " + cur.val);

            for(int i = 0; i < cur.neighborNodes.size(); i++) {
                Node next = cur.neighborNodes.get(i);
                if (!set.contains(next)) {
                    set.add(next);
                    stack.add(next);
                }
            }
        }
    }

    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{7,4,7},{4,8,7}, {6,6,4},{7,7,6}};
        Graph graph = GenerateGrap.createGraph(matrix);
        dfs(graph);
    }
}

拓扑序:

1)在图中找到所有入度为0的点输出

2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始

3)图的所有点都被删除后,依次输出的顺序就是拓扑排序

要求:有向图且其中没有环

package code03.图_05;

import code03.图_05.结构.GenerateGrap;
import code03.图_05.结构.Graph;
import code03.图_05.结构.Node;

import java.util.*;

public class TopoSort_03 {

    public static List sort(Graph graph)
    {
        if (graph == null || graph.nodes.size() == 0) {
            return null;
        }

        Queue<Node> queue = new LinkedList<>();
        //使用map可以构造出节点和入度的key-value结构。就算是图的
        //结构中没有入度,我们也可以通过map给强行构造出来
        Map<Node, Integer> map = new HashMap<>();
        //搜集所有入度为0的顶点
        for(Node node : graph.nodes.values()) {
            map.put(node, node.in);
            if (node.in == 0) {
                queue.add(node);
            }
        }

        //拓扑结果必须是有向、无环的图
        if (queue.isEmpty()) {
            return null;
        }

        List<Node> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            Node cur  =  queue.poll();
            result.add(cur);    //搜集到入度为0的节点

            //当前节点指向的节点,入度都要减少1
            for (Node next : cur.neighborNodes) {
                map.put(next, map.get(next)-1);
                if (map.get(next) == 0) {
                    queue.add(next);
                }
            }
        }

        return result;
    }

    //与上一个方法,唯一的不同就是没有使用map构造节点和入度的key-value
    public static List sort2(Graph graph)
    {
        if (graph == null || graph.nodes.size() == 0) {
            return null;
        }

        Queue<Node> queue = new LinkedList<>();
        //搜集所有入度为0的顶点
        for(Node node : graph.nodes.values()) {
            if (node.in == 0) {
                queue.add(node);
            }
        }

        //拓扑结果必须是有向、无环的图。如果找不到入度为0
        //的顶点,那就说明是有环
        if (queue.isEmpty()) {
            return null;
        }

        List<Node> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            Node cur  =  queue.poll();
            result.add(cur);    //搜集到入度为0的节点
            
            //当前节点指向的节点,入度都要减少1
            for (Node next : cur.neighborNodes) {
                next.in--;
                if (next.in == 0) {
                    queue.add(next);
                }
            }
        }

        return result;
    }

    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{8,3,7},{5,4,6},{4,8,7}, {7,7,6}};
        Graph graph = GenerateGrap.createGraph(matrix);
        //List<Node> list = sort(graph);
        List<Node> list = sort2(graph);
        for (Node node : list) {
            System.out.println(node.val);
        }
    }
}

LintCode算法题

下面刷一道算法题,体现一下使用Map构造Node和入度的key-value结构是多么的重要。题目的详细说明看连接  https://www.lintcode.com/problem/127/

这题的最大障碍是他自己定义了点结构,并且这个结构中没有入度、出度这个概念。上一题拓扑序我们是直接使用点结构中的入度,就可以直接锁定入度为0的节点,并且可以根据这个入度参数,直接进行节点删除后,它的指向节点的入度减少操作。

本题,我们定义一个Map来记录它的节点和入度的 key-value结构就可以解决。

package code03.图_05;

import unit2.class33.Hash;

import java.util.*;

/**
 * https://www.lintcode.com/problem/127/
 *
 * 最大的问题是没有入度
 */
public class TopoSortLintCode {

    //图中的点结构
    static class DirectedGraphNode {
        int label;
        List<DirectedGraphNode> neighbors;

        DirectedGraphNode(int x) {
            label = x;
            neighbors = new ArrayList<DirectedGraphNode>();
        }
    }

    public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph)
    {
        if (graph == null || graph.size() == 0) {
            return null;
        }

        Map<DirectedGraphNode, Integer> inMap = new HashMap<>();

        //构建每一个节点的入度
        for(DirectedGraphNode node : graph) {
            //默认都为0的入度
            inMap.put(node, 0);
        }

        //按照节点的直接指向,给每一个指向节点设置入度值
        for(DirectedGraphNode node : graph) {
             for (DirectedGraphNode next : node.neighbors) {
                 inMap.put(next, inMap.get(next) + 1);
             }
        }

        //找到默认入度为0的顶点
        Queue<DirectedGraphNode> queue = new LinkedList<>();
        for(DirectedGraphNode node : graph) {
            if (inMap.get(node) == 0) {
                queue.add(node);
            }
        }

        //拓扑排序结果搜集
        ArrayList<DirectedGraphNode> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            DirectedGraphNode node = queue.poll();
            result.add(node);
            for (DirectedGraphNode next : node.neighbors) {
                inMap.put(next, inMap.get(next) -1);
                if (inMap.get(next) == 0) {
                     queue.add(next);
                }
            }
        }

        return result;
    }


    public static void main(String[] args) {
        //输入 graph = {0,1,2,3#1,4#2,4,5#3,4,5#4#5}
        //输出 [0, 1, 2, 3, 4, 5]

        ArrayList<DirectedGraphNode> graph = new ArrayList<>();
        DirectedGraphNode node0 = new DirectedGraphNode(0);
        DirectedGraphNode node1 = new DirectedGraphNode(1);
        DirectedGraphNode node2 = new DirectedGraphNode(2);
        DirectedGraphNode node3 = new DirectedGraphNode(3);
        DirectedGraphNode node4 = new DirectedGraphNode(4);
        DirectedGraphNode node5 = new DirectedGraphNode(5);
        node0.neighbors.add(node1);
        node0.neighbors.add(node2);
        node0.neighbors.add(node3);

        node1.neighbors.add(node4);
        node2.neighbors.add(node4);
        node2.neighbors.add(node5);

        node3.neighbors.add(node4);
        node3.neighbors.add(node5);

        graph.add(node0);
        graph.add(node1);
        graph.add(node2);
        graph.add(node3);
        graph.add(node4);
        graph.add(node5);

        TopoSortLintCode code = new TopoSortLintCode();
        ArrayList<DirectedGraphNode> nodes = code.topSort(graph);
        for (DirectedGraphNode node : nodes) {
            System.out.println(node.label);
        }
    }
}

我测试了一下,在LintCode中打败了84%的选手,也就是还有优化的空间,下面看优化版本:

package code03.图_05;

import java.util.*;

/**
 * https://www.lintcode.com/problem/127/
 *
 * 优化版本
 */
public class TopoSortLintCode_opt {

    //图中的点结构
    static class DirectedGraphNode {
        int label;
        List<DirectedGraphNode> neighbors;

        DirectedGraphNode(int x) {
            label = x;
            neighbors = new ArrayList<DirectedGraphNode>();
        }
    }

    public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph)
    {
        if (graph == null || graph.size() == 0) {
            return null;
        }

        Map<DirectedGraphNode, Integer> inMap = new HashMap<>();
        //按照节点的直接指向,给每一个指向节点设置入度值。 此处是最大的优先点,
        //是直接把有入度的节点给设置入度值,而不是之前给所有节点设置入度值为0的操作。少了
        //一个for循环,提高了性能
        for(DirectedGraphNode node : graph) {
             //这个for循环都是被指向的节点,因此入度至少为1.
             for (DirectedGraphNode next : node.neighbors) {
                 if (inMap.containsKey(next)) {
                     inMap.put(next, inMap.get(next) + 1);  //多次出现,就是累加
                 }
                 else {
                     inMap.put(next, 1);    //第一次就是1
                 }
             }
        }

        //如果map中没有当前node,说明当前node不是任何node的neighbors元素
        //也就变相说明了它没有入度,即入度为0
        Queue<DirectedGraphNode> queue = new LinkedList<>();
        for(DirectedGraphNode node : graph) {
            if (!inMap.containsKey(node)) {
                queue.add(node);
            }
        }

        //拓扑排序结果搜集
        ArrayList<DirectedGraphNode> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            DirectedGraphNode node = queue.poll();
            result.add(node);
            for (DirectedGraphNode next : node.neighbors) {
                inMap.put(next, inMap.get(next) -1);
                if (inMap.get(next) == 0) {
                     queue.add(next);
                }
            }
        }

        return result;
    }


    public static void main(String[] args) {
        //输入 graph = {0,1,2,3#1,4#2,4,5#3,4,5#4#5}
        //输出 [0, 1, 2, 3, 4, 5]

        ArrayList<DirectedGraphNode> graph = new ArrayList<>();
        DirectedGraphNode node0 = new DirectedGraphNode(0);
        DirectedGraphNode node1 = new DirectedGraphNode(1);
        DirectedGraphNode node2 = new DirectedGraphNode(2);
        DirectedGraphNode node3 = new DirectedGraphNode(3);
        DirectedGraphNode node4 = new DirectedGraphNode(4);
        DirectedGraphNode node5 = new DirectedGraphNode(5);
        node0.neighbors.add(node1);
        node0.neighbors.add(node2);
        node0.neighbors.add(node3);

        node1.neighbors.add(node4);
        node2.neighbors.add(node4);
        node2.neighbors.add(node5);

        node3.neighbors.add(node4);
        node3.neighbors.add(node5);

        graph.add(node0);
        graph.add(node1);
        graph.add(node2);
        graph.add(node3);
        graph.add(node4);
        graph.add(node5);

        TopoSortLintCode_opt code = new TopoSortLintCode_opt();
        ArrayList<DirectedGraphNode> nodes = code.topSort(graph);
        for (DirectedGraphNode node : nodes) {
            System.out.println(node.label);
        }
    }
}

通过优化,同样的数据,测试结果打败100%。

最小生成树

概念我就不去说了,简单介绍就是找到连接图中连接所有顶点的边,并且这些边的权重累加和最小。

Kruskal算法

1)总是从权值最小的边开始考虑,依次考察权值依次变大的边

2)当前的边要么进入最小生成树的集合,要么丢弃

3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边

4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边

5)考察完所有边之后,最小生成树的集合也得到了

K算法的描述已经很清楚了,就是搜集最小边,无环就留下,有环就pass掉。而边是有开始和结束两个顶点组成的,根据from-to这两个顶点,就可以确认一条边了。以三角形3个顶点 A B C举例。 AB BC被搜集了,那么AC或者CA再出现, 就会形成环。典型的并查集思想。

package code03.图_05;

import code03.图_05.结构.Edge;
import code03.图_05.结构.GenerateGrap;
import code03.图_05.结构.Graph;
import code03.图_05.结构.Node;

import java.util.*;

/**
 * 最小生成树算法之Kruskal
 *
 * 1)总是从权值最小的边开始考虑,依次考察权值依次变大的边
 * 2)当前的边要么进入最小生成树的集合,要么丢弃
 * 3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边
 * 4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边
 * 5)考察完所有边之后,最小生成树的集合也得到了
 *
 * 使用并查集的相关算法解决
 */
public class KruskalMinTree_04 {

    static class UnionFind {

        Map<Node, Node> parent;
        Map<Node, Integer> size;

        public UnionFind (Collection<Node> nodes) {
            parent = new HashMap<>();
            size = new HashMap<>();

            for (Node node : nodes) {
                parent.put(node, node);
                size.put(node, 1);
            }
        }

        public Node findParent (Node cur) {
            Node node = cur;
            /**
             * 默认cur == parent.get(cur) 如果不相等
             * 说明并查集合并过
             */
            while (cur != parent.get(cur)) {
                cur = parent.get(cur);
            }
            return cur;
        }

        //判断2个顶点是否被合并在同一个集合中
        public boolean isSameCollection (Node node1, Node node2) {
            return findParent(node1) == findParent(node2);
        }

        public void union (Node from, Node to) {
            Node fParent = findParent(from);
            Node tParent = findParent(to);

            //没有共同的祖先,说明不在同一个集合中
            if (fParent != tParent) {


                /*if (size.get(fParent) >= size.get(tParent)) {
                    //顶点数小节点,挂在顶点数大的节点下方
                    parent.put(tParent, fParent);
                    //更新大集合下方顶点数量
                    size.put(fParent, size.get(fParent) + size.get(tParent));
                    //被合并后的集合,不再保有后代信息
                    size.put(tParent, 1);   //0 或 1 都不影响
                }
                else {
                    parent.put(fParent, tParent);
                    //更新大集合下方顶点数量
                    size.put(tParent, size.get(fParent) + size.get(tParent));
                    size.put(fParent, 1);
                }*/

                //上方if...else的优化版本
                Node maxParent = size.get(fParent) >= size.get(tParent) ? fParent : tParent;
                Node minParent = maxParent == fParent ? tParent : fParent;
                //顶点数小节点,挂在顶点数大的节点下方. 也就是说它的父为顶点大的节点
                parent.put(minParent, maxParent);
                //更新大集合下方顶点数量
                size.put(maxParent, size.get(maxParent) + size.get(minParent));
                //被合并后的集合,不再保有后代信息
                size.put(minParent, 1);   //0 或 1 都不影响
            }
        }
    }

    //升序比较器
    class MyComparator implements Comparator<Edge> {
        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }
    }

    public Set<Edge> kruskalMST(Graph graph)
    {
        if (graph == null || graph.nodes.values().isEmpty()) {
            return null;
        }

        Set<Edge> set = graph.edges;    //边
        HashMap<Integer, Node> map = graph.nodes;   //顶点
        UnionFind uf = new UnionFind(map.values());

        //小根堆,是为了搜集权重较小的边
        Queue<Edge> queue = new PriorityQueue<>(new MyComparator());

        //所有的边,按照由小到大排序
        for(Iterator iterator = set.iterator(); iterator.hasNext();)
        {
            Edge edge = (Edge) iterator.next();
            queue.add(edge);
        }

        Set<Edge> minTreeEdge  = new HashSet();
        /**
         * 由小到大遍历所有的边。
         * 如果边的开头-结尾顶点不在同一个集合,说明边是我们需要的最小生成树的边
         * 如果边的开头-顶点已经在同一个集合,那就不能添加,否则形成环
         */
        while (!queue.isEmpty()) {
            Edge edge = queue.poll();
            //当前边是最小边,如果边的开头-结尾顶点不在同一个集合,
            //说明这条边还没有被搜集,我们需要搜集这条表。
            //以三角形3个顶点 A B C举例。 AB BC被搜集了,那么AC或者CA再出现,
            //就会形成环。
            if (!uf.isSameCollection(edge.from, edge.to)) {
                //搜集到的边
                minTreeEdge.add(edge);
                uf.union(edge.from, edge.to);
            }
        }

        return minTreeEdge;
    }

    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{7,4,7},{11,8,7}};
        Graph graph = GenerateGrap.createGraph(matrix);

        KruskalMinTree_04 k = new KruskalMinTree_04();
        Set<Edge> set = k.kruskalMST(graph);

        for(Iterator iterator = set.iterator(); iterator.hasNext();) {
            Edge edge = (Edge) iterator.next();
            System.out.println("当前边的长度为 :" + edge.weight + ", from节点:" + edge.from.val + ", to节点为:" + edge.to.val);
        }
    }
}

Prim算法

1)可以从任意节点出发来寻找最小生成树

2)某个点加入到被选取的点中后,解锁这个点出发的所有新的边

3)在所有解锁的边中选最小的边,然后看看这个边会不会形成环

4)如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3

5)如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2

6)当所有点都被选取,最小生成树就得到了

简单概括就是,搜集到一个顶点,就解锁这个点的所有边,把这些边和之前已经解锁的边放在一起比较,找到最小的边。无环,就搜集。有环,就pass掉。

package code03.图_05;

import code03.图_05.结构.Edge;
import code03.图_05.结构.GenerateGrap;
import code03.图_05.结构.Graph;
import code03.图_05.结构.Node;

import java.util.*;

/**
 * 最小生成树算法之Prim
 *
 * 1)可以从任意节点出发来寻找最小生成树
 * 2)某个点加入到被选取的点中后,解锁这个点出发的所有新的边
 * 3)在所有解锁的边中选最小的边,然后看看这个边会不会形成环
 * 4)如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
 * 5)如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
 * 6)当所有点都被选取,最小生成树就得到了
 */
public class PrimMinTree_05 {

    static class EdgeComparator implements Comparator<Edge> {
        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }
    }

    public Set<Edge> primMST(Graph graph)
    {
        if (graph == null || graph.nodes.values().isEmpty()) {
            return null;
        }

        // 解锁的边进入小根堆
        Queue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        //哪些点被解锁出来了
        HashSet<Node> nodeSet = new HashSet<>();
        // 依次挑选的的边在result里. 这里的set是核心,因为它不会存在重复元素,是会被替换的。
        Set<Edge> result = new HashSet<>();

        //1)可以从任意节点出发来寻找最小生成树
        for (Node node : graph.nodes.values())
        {
            if (!nodeSet.contains(node)) {
                nodeSet.add(node);
                //2)某个点加入到被选取的点中后,解锁这个点出发的所有新的边
                for (Edge edge : node.neighborEdges) {
                    priorityQueue.add(edge);
                }

                while (!priorityQueue.isEmpty()) {
                    //3)在所有解锁的边中选最小的边,然后看看这个边会不会形成环
                    Edge edge = priorityQueue.poll();
                    Node toNode = edge.to;
                    /**
                     * 4)如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
                     * 5)如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
                     * 
                     * 这里需要强调的是,为什么nodeSet中不含有这个toNode节点,那这条边就是无环的呢?
                     * 首先,这是一条最小的边,待定选择;
                     * 然后,需要判断这条边是否会形成环,以三角形为例。 A B C 3个点
                     * 
                     * 1. 首先解锁A节点,得到AB AC 两条边。 假设AB边的权重比较小。 选择AB这条边,
                     * B节点没有呗解锁过。 那么久要AB这条边。 
                     * 2. 然后解锁B这个点对应的所有边。此时发现, AB被解锁过了,BC没有呗解锁。
                     * AC和BC这2条边选取小的。 假设BC比较小,选取BC
                     * 3. C这个点没有呗解锁过, BC被搜集
                     * 4 最后剩下的就是CA这条表了,它此时也是最小的边。C点刚被解锁,发现A点已经被解锁了,
                     * 如果旋转CA这条边,就形成环。因此放弃这条。最终就是AB BC这2条边被选中
                     * 
                     * 其实,第4步,AC或者CA都行,如果AC,那就是A节点刚被解锁过,去看C节点,此时C节点也被
                     * 解锁过,会形成环,直接放弃。道理是一样的
                     */
                    if (!nodeSet.contains(toNode)) {
                        nodeSet.add(toNode);
                        //当前边是最小边,并且当前边的from-to都是第一次解锁
                        //说明这就是要搜集的边。
                        result.add(edge);
                        //5)将该边的指向点加入到被选取的点中,重复2)
                        for (Edge nextEdge : toNode.neighborEdges) {
                            priorityQueue.add(nextEdge);
                        }
                    }
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        int[][] matrix = {{3,1,2},{4,1,3},{2,1,4}, {5,2,8},{2,3,8},{7,4,7},{11,8,7}};
        Graph graph = GenerateGrap.createGraph(matrix);

        PrimMinTree_05 p = new PrimMinTree_05();
        Set<Edge> set = p.primMST(graph);

        for(Iterator iterator = set.iterator(); iterator.hasNext();) {
            Edge edge = (Edge) iterator.next();
            System.out.println("当前边的长度为 :" + edge.weight + ", from节点:" + edge.from.val + ", to节点为:" + edge.to.val);
        }
    }
}

举报

相关推荐

0 条评论