【算法】006_图

图的表示

图的表示方法有很多种,如果遇到不同的表示方法,可以转换成自己最常用的那一种

public class Graph {
    public HashMap<Integer, Node> nodes;//点集
    public HashSet<Edge> edges;//边集

    public Graph() {
    }

    public Graph(HashMap<Integer, Node> nodes, HashSet<Edge> edges) {
        this.nodes = nodes;
        this.edges = edges;
    }

}

public class Edge {
    public int weight;//边的距离
    public Node from;
    public Node to;

    public Edge(int weight, Node from, Node to) {
        this.weight = weight;
        this.from = from;
        this.to = to;
    }
}

public class Node {
    public int value;//点的值
    public int in;//点的入度
    public int out;//点的出度
    public ArrayList<Node> nextList;//点指向哪些点
    public ArrayList<Edge> edgeList;//点的出边

    public Node(int value){
        this.value = value;
        in = 0;
        out = 0;
        nextList = new ArrayList<>();
        edgeList = new ArrayList<>();
    }
}

public class GraphGenerator {
    public Graph createGraph(Integer[][] matrix){
        Graph graph = new Graph();
        for (int i = 0; i < matrix.length; i++) {
            Integer from = matrix[i][0];//出点
            Integer to = matrix[i][1];//入点
            Integer weight = 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 newEdge = new Edge(weight, fromNode, toNode);
            fromNode.nextList.add(toNode);
            fromNode.out++;
            toNode.in++;
            fromNode.edgeList.add(newEdge);
            graph.edges.add(newEdge);
        }
        return graph;
    }
}

宽度优先遍历BFS

  • 利用队列实现
  • 从源节点开始依次按照宽度进队列,然后弹出
  • 每弹出一个结点,就把该结点所有没有进过队列的邻接点放入队列
  • 直到队列为空
public class Code01_BFS {
    public void bfs(Node node){
        if (node == null){
            return;
        }
        Queue<Node> queue = new LinkedList<>();
        HashSet<Node> set = new HashSet<>();//用于标记访问过的结点
        queue.add(node);
        set.add(node);
        while (!queue.isEmpty()){
            Node cur = queue.poll();
            System.out.println(cur.value);
            for (Node next : cur.nexts) {
                if (!set.contains(next)){
                    set.add(next);
                    queue.add(next);
                }
            }
        }
    }
}

深度优先遍历DFS

  • 利用栈实现
  • 从源节点开始把结点按照深度放入栈,然后弹出
  • 每弹出一个点,就把该结点下一个没有进过栈的邻接点放入栈
  • 直到栈为空
public class Code02_DFS {
    public void dfs(Node node){
        if (node == null){
            return;
        }
        Stack<Node> stack = new Stack<>();
        HashSet<Node> set = new HashSet<>();
        stack.add(node);
        set.add(node);
        System.out.println(node.value);
        while (!stack.isEmpty()){
            Node cur = stack.pop();
            for (Node next : cur.nexts) {
                //只要是没有访问过的结点就访问,一直走到底
                //栈中保存的就是当前的DFS的路径
                if (!set.contains(next)){
                    stack.push(cur);
                    stack.push(next);
                    set.add(next);
                    System.out.println(next.value);
                    break;//访问下一个邻接点
                }
            }
        }
    }
}

拓扑排序算法

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

  • 从第一个结点node出发,访问node

  • 去掉图上的node,同时删除与node的邻接点之间的边,这些邻接点的入度就会减一

  • 然后访问下一个入度为0的结点

public class Code03_TopologySort {
    public List<Node> topologySort(Graph graph){
        //key:某一个node;value:剩余的入度
        HashMap<Node, Integer> inMap = new HashMap<>();
        //保存入度为0的结点
        Queue<Node> zeroInQueue = new LinkedList<>();
        //将所有结点放入哈希表中
        for (Node node : graph.nodes.values()) {
            inMap.put(node, node.in);
            if (node.in == 0){
                zeroInQueue.add(node);
            }
        }
        //拓扑排序的结果依次加入result
        List<Node> result = new ArrayList<>();
        while (!zeroInQueue.isEmpty()){
            Node cur = zeroInQueue.poll();
            result.add(cur);
            //去掉这个入度为0的结点后,它的邻接点的入度也要减一
            for (Node next : cur.nexts) {
                inMap.put(next, inMap.get(next) - 1);//更新
                if (inMap.get(next) == 0){
                    zeroInQueue.add(next);
                }
            }
        }
        return result;
    }
}

kruskal算法

适用范围:要求无向图

作用:生成最小生成树

  • 每次都选择最小的边
  • 选择过程中不能形成环
  • 直到形成了最小生成树

image-20240215133543305

  • 初始时,每个结点单独一个集合:{A},{B},{C},

  • 找到最小的边为2,2连接了A和C,A和C不在同一个集合,于是把他们加入一个集合:{A,C},{B},

  • 考察权值为4的边,发现D和C不在一个集合,合并:{A,C,D},

  • 考察权值为7的边,发现A和B不在一个集合,合并:

  • 考察权值为100的边,形成了环,过

    如何判断是否有环:

    • 边的两个结点在一个集合,加上这条边之后就会形成环
  • 考察权值为1000的边,形成了环,过

  • 最后选择了权值为2、4、7的三条边

public class Code04_Kruskal {

    public class MySet{
        
        //key:结点;value:结点对应的集合
        public HashMap<Node, List<Node>> setMap;

        public MySet(List<Node> nodes){
            //每个结点对应的集合
            //最开始的时候一个结点一个集合
            for (Node cur : nodes){
                List<Node> set = new ArrayList<>();
                set.add(cur);
                setMap.put(cur, set);
            }
        }

        //判断两个结点是否属于一个集合
        public boolean isSameSet(Node from, Node to){
            List<Node> fromSet = setMap.get(from);
            List<Node> toSet = setMap.get(to);
            return fromSet == toSet;//一个集合
        }

        //一条边的两个结点,如果不在一个集合就合并
        public void union(Node from, Node to){
            List<Node> fromSet = setMap.get(from);//from所在的结点
            List<Node> toSet = setMap.get(to);//to所在的结点
            for (Node toNode : toSet) {
                fromSet.add(toNode);
                setMap.put(toNode, fromSet);
            }
        }
    }

    public class EdgeComparator implements Comparator<Edge> {

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

    }

    public Set<Edge> kruskalMST(Graph graph){
        //初始化,每个结点对应一个集合
        MySet mySet = new MySet(graph.nodes.values().stream().toList());
        //边按照从小到大排序
        PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        for (Edge edge : graph.edges) {
            priorityQueue.add(edge);
        }
        Set<Edge> result = new HashSet<>();
        while (!priorityQueue.isEmpty()){
            Edge edge = priorityQueue.poll();//获取最短边
            //判断edge的两个结点是否属于一个集合
            //如果属于一个集合,加上这条边,这个图就会有环
            //如果不属于一个集合,就合并
            if (!mySet.isSameSet(edge.from, edge.to)){
                result.add(edge);
                mySet.union(edge.from, edge.to);//合并
            }
        }
        return result;
    }
}

prim算法

适用范围:要求无向图

作用:生成最小生成树

  • 集合用于保存访问过的结点
  • 每次都选择的是集合中的结点的所有邻接边中,最短的那条边,所以可以使用小根堆
  • 选中的边就会有一个新的结点,这个结点就加入集合中

image-20240215155016961

  • 任意选择一个出发点,假设从A出发
  • A的邻接边中AC权值最小:
  • {A,C}两个结点的所有邻接边中CF权值最小:
  • {A,C,F}的邻接边中FD权值最小:
  • {A,C,F,D}的邻接边中CB最短:
  • {A,C,F,D,B}的邻接边中BE权值最小:
public class Code05_Prim {
    public class EdgeComparator implements Comparator<Edge> {

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

    }

    public Set<Edge> primMST(Graph graph){
        //每加入一个新的点到集合中,就会解锁新的邻接边
        //解锁的边进入小根堆
        PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());

        HashSet<Node> set = new HashSet<>();

        Set<Edge> result = new HashSet<>();

        // Node node = graph.nodes.values().stream().toList().get(0);//选择一个结点作为开始
        
        for (Node node : graph.nodes.values()) {//可能有多个不连通的图,需要每一个图生成一个最小生成树,这时就需要循环
            //选中的结点不在集合中
            //如果选中的结点在集合中,会形成环
            if (!set.contains(node)){//多个不连通的图的时候需要加上这行
                set.add(node);
                //遍历node的所有邻接边,将边加入到小根堆中
                for (Edge edge : node.edges) {
                    priorityQueue.add(edge);
                }
                while (!priorityQueue.isEmpty()){
                    Edge edge = priorityQueue.poll();//小根堆中的最短边
                    Node toNode = edge.to;//解锁的新节点  edge中fromNode已经在集合中了
                    //判断是否在集合中,如果在的话这个点就不能加入,也就是edge这条边不能要,否则会形成环
                    if (!set.contains(toNode)){
                        set.add(toNode);
                        result.add(edge);
                        for (Edge nextEdge : toNode.edges) {
                            priorityQueue.add(nextEdge);
                        }
                    }
                }
            }
        }
        
        return result;
    }
}

Dijkstra算法

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

作用:从某一个点出发,到其他点的最短距离。其他点要经过已经已知的点。

从一个结点A开始,依次加入结点,更新A到其他结点的最短距离。

image-20240215164627657

初始

image-20240215164737331

  • A有AB、AC、AD三条边
    • 由于这三条边使得A到B、C、D的距离变得更短了

image-20240215164917456

  • A到A的最短距离已经求到了,剩余结点中A到B的距离最近,于是选择B。B有BA、BC、BE三条边

    有了新的边,判断A到其他点的距离是否更短了

    • 从A到B再到C,AC之间的距离变短

      也就是AB的最短距离已经知道,如果B可以到达其他结点,那么距离就会更短

    • 从A到B再到E,AE之间的距离变短

image-20240215165244334

  • 剩余结点A到C的距离最近,选择C。

    判断A经过B、C到其他结点是否距离更短

    也就是AC最短距离是5,那么AE=AC+CE=19

    如果C可以到达其他结点,那么A到其他结点的距离就可能会更新

    image-20240215165832920

public class Code06_Dijkstra {

    public HashMap<Node, Integer> dijkstra1(Node head){
        //从head出发到所有点的最小距离
        //key:从head出发到达key
        //value:从head出发到达key的最小距离
        //如果在表中,没有T的记录,含义是从head出发到T点的距离是正无穷
        HashMap<Node, Integer> distanceMap = new HashMap<>();
        distanceMap.put(head, 0);
        //已经求过距离的结点,存在selectedNodes中,以后再也不碰
        HashSet<Node> selectedNodes = new HashSet<>();
        //从distanceMap中选择最短的路径,并且是没有被使用过的,没有被当作中间结点,更新head到其他结点的距离
        Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
        while (minNode != null){
            int distance = distanceMap.get(minNode);//head到minNode的最短距离
            //已经知道了head到minNode的最短距离,那么其它结点从head经过minNode到达之后距离是否更短
            for (Edge edge : minNode.edges) {
                Node toNode = edge.to;
                if (!distanceMap.containsKey(toNode)){//说明head到toNode是正无穷
                    distanceMap.put(toNode, distance + edge.weight);
                }
                //原来的距离 与 加入新节点之后的距离进行比较
                distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
            }
            selectedNodes.add(minNode);//minNode被使用过了
        }
        return distanceMap;
    }

    public Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes){
        Node minNode = null;
        int minDistance = Integer.MAX_VALUE;
        for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {
            Node node = entry.getKey();
            int distance = entry.getValue();
            if (!touchedNodes.contains(node) && distance < minDistance){
                minDistance = distance;
                minNode = node;
            }
        }
        return minNode;
    }
}
posted @ 2024-02-15 18:01  平平无奇的five  阅读(146)  评论(0)    收藏  举报