【数据结构与算法】第十七篇:图论万字解析(进阶篇)

news2024/11/16 13:30:25

👀作者简介:大家好,我是 快到碗里来~
🚩🚩 个人主页:快到碗里来~
支持我:点赞+关注~不迷路🧡🧡🧡
✔系列专栏:数据结构与算法⚡⚡⚡
(❁´◡`❁)励志格言:在我有生之日,做一个真诚的人,不放弃对生活的热爱和执着,在有限的时空里,过无限广大的日子。(by 三毛)🤞🤞
在这里插入图片描述

知识导航

  • 一.图的基本实现前导算法
    • 1.代码结构设计
    • 2.添加节点(addVertex)
    • 3.添加边(addEdge)
    • 4.删除节点(removeVertex)
    • 5.删除边(removeEdge)
  • 二、图的遍历
    • 1.bfs算法
    • 2.dfs算法
    • (1)递归实现
    • (2)非递归实现
    • 3.leetcode精选例题
  • 三.最小生成树
    • 1.普利姆(prim)算法
    • 2.克鲁斯尔(kruskal)算法
  • 四.最短路径
    • (1)最短路径之无权图
    • (2)最短路径之负权环
    • 算法实现
      • 1 .Dijkstra(迪杰斯特拉算法)
        • 算法思想具体形象化(缓慢拉绳子案例)
      • 2. Bellman-Ford(贝尔曼-福特算法)
    • 五.拓扑排序
      • 1.AOV网(Activity On Vertex Network)
      • 2.拓扑排序原理


一.图的基本实现前导算法

1.代码结构设计

上节基础篇篇我们提到了图的两种实现方法–>临接矩阵实现法以及邻接表实现法。并分析了他们各自的利弊。
在这里插入图片描述
所以为了权衡他们的利弊,我们尝试用一种介于邻接矩阵和邻接表之间实现的方法,但从实现形式上他更偏向于邻接表的一种方法。😎😎
1.在图的实现类中定义一个哈希表和哈希集合分别存储图中所有的点和边。

//将节点的值和节点联系起来,(存放所有的节点)
private final Map<V ,Vertex<V,E>> vertexes =new HashMap<>();
//edges存放所有的边,为什么用集合因为inEdges和outEdges有重复避免出现重复节点
private final Set<Edge<V,E>> edges=new HashSet<>();

2.实现节点类(注意在节点类中我们有定义了两个集合分别存储节点的出度和入度的边兼容了无向图和有向图)

在节点类中重写了equals()和hashCode()方法使他具备可以比较的条件

 //V自身顶点的类型,E顶点上权值的类型
    private static class Vertex<V,E>{

        public Vertex(V val) {
            this.val = val;
        }

        V val;
        //分别存放入,出节点的边
        //节点的出入线不考虑顺序所以用Set更合适
        Set<Edge<V,E>> inEdges=new HashSet<>();
        Set<Edge<V,E>> outEdges=new HashSet<>();
        //边的比较是基于节点的所以节点也要重写equals()和hashcode()

        //val值相等就认为两个节点相等
        @Override
        public boolean equals(Object obj) {
            return Objects.equals(val,((Vertex<V,E>)obj).val);
        }

        @Override
        public int hashCode() {
            return val==null?0:val.hashCode();
        }

        @Override
        public String toString() {
            return val== null ? "null" : val.toString();
        }
    }

3.实现边(Edge类)
这里重写了equals()方法,只要一条边的起始节点和终止节点相等我们就认为这两条边相等。 toInfo()方法实现了Edge类和EdgeInfo之间的转换,EdgeInfo是自定义存储边的信息的接口->是返回给外界的公共接口。

//V边所连节点的类型,E边上的权值
    private  static class Edge<V,E> {
        public Edge(Vertex<V, E> from, Vertex<V, E> to) {
            this.from = from;
            this.to = to;
        }

        //该线的尾部
        Vertex<V,E> from;
        //该线指向的节点
        Vertex<V,E> to;
        E weight;

        @Override
        public boolean equals(Object obj) {
        //判断边的相等归根结底是判断节点的相等
            Edge<V,E> edge=(Edge<V,E>)obj;
            return Objects.equals(edge.from,from)&&Objects.equals(edge.to,to);
        }

        @Override
        public int hashCode() {
            //自己定义哈希表的规则,这里写31是借鉴字符串的哈希值的转换规则,是哈希表更加均匀
            return from.hashCode()*31+to.hashCode();
        }

        @Override
        public String toString() {
            return "Edge{" +
                    "from=" + from +
                    ", to=" + to +
                    ", weight=" + weight +
                    '}';
        }
        //Edge与EdgeInfo之间的比较
        public  EdgeInfo<V,E> toInfo(){
            //Cannot infer arguments 错误分析:检查泛型的类型对应
            return new EdgeInfo<>(from.val,to.val,weight);
        }

    }

4.公共方法和接口抽调为抽象类

package grapth;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

abstract public class Graph<V,E> {
    public Graph(){}
    //权值比较
    //自定义比较方法传入
    protected WeightManager<E> weightManager;
    public Graph(WeightManager<E> e){
        this.weightManager=e;
    }

    public abstract int edgesSize();
    public abstract int verticesSize();

    public abstract void addVertex(V v);
    public abstract void addEdge(V from, V to);
    public abstract void addEdge(V from, V to, E weight);

    public abstract void removeVertex(V v);
    public abstract void removeEdge(V from, V to);
    //广度遍历
    //传入控制访问器
    public abstract void bfs(V begin,VertexVisitor<V>visitor);
    public abstract void dfs(V begin,VertexVisitor<V>visitor);
    public abstract void dfs2(V begin,VertexVisitor<V>visitor);
    //返回拓扑序列(打印顺序)
    public abstract List<V> topologicalSort();
    public abstract Set<EdgeInfo<V,E>> mst();
    public abstract Map<V,pathInfo<V,E>> shortedPath(V v);

    //路径信息
    public static class pathInfo<V,E>{
        protected E weight;//存放路径权值之和
        protected List<EdgeInfo<V,E>> edgeInfos=new LinkedList<>();//存放路径边的信息

        public void setWeight(E weight) {
            this.weight = weight;
        }
        public E getWeight(){
            return weight;
        }

        public List<EdgeInfo<V, E>> getEdgeInfos() {
            return edgeInfos;
        }

        public void setEdgeInfos(List<EdgeInfo<V, E>> edgeInfos) {
            this.edgeInfos = edgeInfos;
        }
        public pathInfo(){}

        @Override
        public String toString() {
            return "pathInfo{" +
                    "weight=" + weight +
                    ", edgeInfos=" + edgeInfos +
                    '}';
        }
    }

    public  interface WeightManager<E>{
        int compare(E e1, E e2);
        E add(E e1,E e2);
        E zero();
    }


  public  static class EdgeInfo<V,E>{
        V from;
        V to;
        E weight;

        public EdgeInfo(){}
        public EdgeInfo(V from, V to, E weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }

      @Override
      public String toString() {
          return "EdgeInfo{" +
                  "from=" + from +
                  ", to=" + to +
                  ", weight=" + weight +
                  '}';
      }
  }
    interface VertexVisitor<V>{
        boolean visit(V v);
    }

}

2.添加节点(addVertex)

在添加节点中先前建立的储存所有节点的哈希表就派上用场了,如果表中本来就存在这个节点就没有必有二次添加,因为哈希表是不会自动去重的

 @Override
    public void addVertex(V v) {
        if(vertexes.containsKey(v)){
            return;
        }
        vertexes.put(v,new Vertex<>(v));

    }

3.添加边(addEdge)

在添加边过程中要考虑到边的起始节点在图中是否一开始就存在,如果两个节点本身就存在则只需链接成边,如果有权值的话更新权值即可
如果两个节点本身不存在则需要重新创建节点。最后不管哪种情况都需要重新更新节点的入度出度信息。

 //服务于无向图
    @Override
    public void addEdge(V from, V to) {
        //有向图去掉权值就是无项图的边
        addEdge(from,to,null);

    }
    //服务于无向图
    @Override
    public void addEdge(V from, V to, E weight) {
        //先保证两个value的节点都存在
        Vertex<V,E> fromVertex=vertexes.get(from);
        if(fromVertex==null)
        {
            fromVertex=new Vertex<>(from);
            vertexes.put(from,fromVertex);
        }
        Vertex<V,E> toVertex=vertexes.get(to);
        if(toVertex==null)
        {
            toVertex=new Vertex<V,E>(to);
            vertexes.put(to,toVertex);
        }
        //保证顶点一定存在了
        Edge<V,E> edge=new Edge<V,E>(fromVertex,toVertex);
        edge.weight=weight;
        /**
         * 问:edges是创建的节点怎么回在集合中找到?
         * 这就是重写equals的意义了,这里在比较的时候不是比较的内存地址,是比较的起始地址是否相等
         *
         */
        //这里的contains底层时调用了equals()方法,所以要重写比较规则和哈希值
        //如果这里能够删除就代表起始点之间已经存在边了,这里需要更新的就是权值
        if(fromVertex.outEdges.remove(edge)){
            //返回boolean值,为true这个点一定存在
            //toEdges和outEdges是同一个线都将其删除
            toVertex.inEdges.remove(edge);
            edges.remove(edge);
        }
        //将带有最新权值的边加入(一删一添加的过程中其实是更新了权值)
        fromVertex.outEdges.add(edge);
        toVertex.inEdges.add(edge);
        edges.add(edge);
    }

4.删除节点(removeVertex)

这里需要注意的是在删除节点过程中,这个节点的入度和出度的边都会删除,与出度,入度的边对应的edge.to(边的终点的边)的入度也需要更新。
在一边遍历一边删除过程中肯定会出现问题(注意一边遍历一边删除大概率会出现问题)所以在进行一边遍历一边删除的操作,一般用迭代器进行执行。

 @Override
    public void removeVertex(V v) {
        //删除并返回value
        Vertex<V,E> vertex=vertexes.remove(v);
        if(vertex==null)return;

        /*边遍历边删除从逻辑上向很有可能出现问题,java中一边遍历一边删除要用迭代器
        vertex.outEdges.forEach(( Edge<V,E>edge)->{
            //删除边就是删除两端的节点
            removeEdge(edge.from.val,edge.to.val);
        });*/
        /*
        1.Iterator<Edge<V,E>> iterator=vertex.outEdges.iterator()
        .iterator()告诉迭代器接收vertex.outEdges,接收类型为Edge
        2.iterator.hasNext()类似于for循环遍历outEdges
         */

        //一边遍历一边删除用迭代器
       for(Iterator<Edge<V,E>> iterator=vertex.outEdges.iterator();iterator.hasNext();){
           //获得遍历中的边
            Edge<V,E> edge=iterator.next();
            //删除该边指向节点的入度操作不受影响,因为遍历的是outEdges.iterator()
            edge.to.inEdges.remove(edge);
            edges.remove(edge);
           //删除outEdges,因为受到边遍历边删除的影响所以讲它交给迭代器进行删除(这个方法会帮你做好一切)
           iterator.remove();

       }
        for(Iterator<Edge<V,E>> iterator=vertex.inEdges.iterator();iterator.hasNext();){
            //获得遍历中的边
            Edge<V,E> edge=iterator.next();
            //删除outEdges,因为受到边遍历边删除的影响所以讲它交给迭代器进行删除(这个方法会帮你做好一切)
            iterator.remove();

            //删除该边指向节点的入度操作不受影响,因为遍历的是outEdges.iterator()
            edge.from.outEdges.remove(edge);
            edges.remove(edge);
        }
    }

5.删除边(removeEdge)

@Override
    public void removeEdge(V from, V to) {
        //返回值是from的节点
        Vertex<V,E> fromVertex=vertexes.get(from);
        if(fromVertex==null)return;
        Vertex<V,E> toVertex=vertexes.get(to);
        if(toVertex==null)return;
        //确定这个边已经存在了
        //新创建的边怎么会找到?判定两个边相等只是看起始点是不是相同
        Edge<V,E> edge=new Edge<V,E>(fromVertex,toVertex);
        if(fromVertex.outEdges.remove(edge)){
            //返回boolean值,为true这个点一定存在
            //toEdges和outEdges是同一个线都将其删除
            toVertex.inEdges.remove(edge);
            edges.remove(edge);
        }
    }

二、图的遍历

◼ 图的遍历
从图中某一顶点出发访问图中其余顶点,且每一个顶点仅被访问一次
◼ 图有2种常见的遍历方式(有向图、无向图都适用)
广度优先搜索(Breadth First Search,BFS),又称为宽度优先搜索、横向优先搜索
深度优先搜索(Depth First Search,DFS)
✓ 发明“深度优先搜索”算法的2位科学家在1986年共同获得计算机领域的最高奖:图灵奖

1.bfs算法

bfs算法本质山和二叉树的层序遍历是一种方式,二叉树本质上也属于图的范畴。
🔥🔥bfs算法也是按照图的每一层进行遍历,那么图是如何区分第几层的呢?
答:根据每层节点的出度相连的节点(也就是说根据每一层节点的出度相连的节点化作下一层)
在这里插入图片描述上面在节点类(Vertex)的设计中我们了解到每个节点都有两个集合outEdges 和 inEdges分别存储出度的边和入度的边,所以我们只要顺着起始节点遍历outEdges
集合拿到出度的边,再由出度的边就可以拿到下一层节点(edge.to).

注意:再进行每个节点outEdges的遍历的过程中,对于无向图肯定会有边产生重复遍历。所以要定义一个visited集合存放已经遍历过的边。

再实现过程中我们同样借助队列实现每有一个节点出队就进行遍历outEdges操作。
在这里插入图片描述

  @Override
    public void bfs(V begin,VertexVisitor<V> visitor) {
        //begin从哪一个节点开始遍历
        Vertex<V,E> vertex=vertexes.get(begin);
        if(vertex==null)return;
        //创建标记集合(c是创建标记数组)
        //visited存储已经访问过的的节点
        Set<Vertex<V,E>> visited=new HashSet<>();
        Queue<Vertex<V,E>> queue=new LinkedList<>();
        queue.offer(vertex);
        while(!queue.isEmpty()){
            Vertex<V,E> ver=queue.poll();
           if(visitor.visit(ver.val))return;
            //已经被访问的节点
            visited.add(ver);
            //拉姆达表达式的形式遍历(和for循环一个意思)
            vertex.outEdges.forEach((Edge<V,E> edge)->{
              if(!visited.contains(edge.to))
              {
                  queue.add (edge.to);
                  visited.add(edge.to);
              }

            });

        }

    }

2.dfs算法

(1)递归实现

深度遍历类似于二叉树的前序遍历都可以用递归实现

  @Override
    public void dfs(V begin,VertexVisitor<V> visitor) {
        Vertex<V,E> vertex=vertexes.get(begin);
        if(begin==null)return;
        dfs(vertex,new HashSet<>(),visitor);
    }
    private void dfs(Vertex<V,E> vertex,Set<Vertex<V,E>> visited,VertexVisitor<V> visitor){
        if(visitor.visit(vertex.val))return;
        //为了避免重复调用已经访问过的节点
        visited.add(vertex);
        for (Edge<V,E> edge:vertex.outEdges
        ) {
            //已经访问过的节点不要访问第二遍
            if(visited.contains(edge.to))continue;
            dfs(edge.to,visited,visitor);
        }
    }

(2)非递归实现

再二叉树的学习过程中,我们直到递归的底层是由栈实现的。所以利用栈的相关操作可以替代栈操作。
我们每遍历到一个节点就遍历他的出度的边,然后将edge.to节点和当前节点一起加入到栈中(为什么要把当前节点也加入到栈中?因为在遍历outEdges集合时我们是按照一条路径一直遍历到底的,当一条路径遍历到完后,还需要遍历该节点的其他路径)
假设以 1 为起点
在这里插入图片描述注意在遍历中过也要建立标记集合,用来存储已经访问过的节点。用来防止路径重复访问。


    @Override
    public void dfs2(V begin,VertexVisitor<V> visitor) {
        Vertex<V,E> beginVertex=vertexes.get(begin);
        if(begin==null)return;
        //标记数组
        Set<Vertex<V,E>> visited =new HashSet<>();
        Stack<Vertex<V,E>> stack=new Stack<>();
        stack.push( beginVertex);
        //先访问了起点
        if(visitor .visit(beginVertex.val))return;

        visited.add( beginVertex);
      while(!stack.isEmpty()) {
          Vertex<V, E> vertex1 = stack.pop();
          for (Edge<V, E> edge : vertex1.outEdges) {
              if (visited.contains(edge.to)) continue;

              stack.push(edge.from);
              stack.push(edge.to);

              visited.add(edge.to);
              if (visitor.visit(edge.to.val)) return;
              break;
          }
      }
    }

3.leetcode精选例题

  1. 电话号码的字母组合
    给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
    给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
    在这里插入图片描述

示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]

解题思路
对于2-9的电话号码我们很容易想到映射,将数字和字母对应起来。
形成映射的方法常见的有
1.利用java自带的HashMap形成映射。
2.利用二维数组形成映射。
对于解题思想我们可以将它假想成一颗号码树。(以实例一为例)
在这里插入图片描述

class Solution {
    public List<String> letterCombinations(String digits) {
        LinkedList<String> res=new LinkedList<>();
        if(digits.length()==0||digits==null)return res;
        char[] []tab={
            {'a','b','c'},{'d','e','f'},{'g','h','i'},{'j','k','l'},{'m','n','o'},{'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}
        };

        res.add("");//点睛之笔
        while(res.peek().length()!=digits.length()){
            String s=res.poll();
            char [] str=tab[digits.charAt(s.length())-'2'];
            for(int i=0;i<str.length;i++){
            res.add(s+str[i]);
            }
        }

        return res;
    }
}

三.最小生成树

上节课我们提到连通图的概念---->(无向图任意两个顶点有相互连通的路径)。
🥳🥳这节我们引入生成树的概念。
1.生成树也被称为支撑树
2.连通图的极小连通子图拥有连通图的所有的n个节点,恰好有n-1条边。
在这里插入图片描述

最小生成树
◼ 最小生成树(Minimum Spanning Tree,简称MST)
◼也称为最小权重生成树(Minimum Weight Spanning Tree)、最小支撑树
◼是所有生成树中,总权值最小的那棵(权值是之和是唯一的)
适用于有权的连通图(无向)
问:图的最小生成树是否是唯一的?
答:如果各个边的权值都是唯一的,那么最小生成树是唯一的
如果各个边的权值是相等的或者出现权值相等的边则最小生成树可能是不唯一的。

最小生成树的应用
最小生成树在许多领域都有重要的作用,例如
◼要在 n 个城市之间铺设光缆,使它们都可以通信
◼铺设光缆的费用很高,且各个城市之间因为距离不同等因素,铺设光缆的费用也不同
◼如何使铺设光缆的总费用最低?
实现最小生成树的主要通过普利姆算法和克鲁斯尔算法实现,在进行算法分析的前提我们先引入一个简单的定理–>切分定理
切分定理
◼ 切分(Cut):把图中的节点分为两部分,称为一个切分
◼ 下图有个切分 C = (S, T),S = {A, B, D},T = {C, E}
在这里插入图片描述
◼ 横切边(Crossing Edge):如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边
◼ 比如上图的边 BC、BE、DE 就是横切边
◼ 切分定理:给定任意切分,横切边中权值最小的边必然属于最小生成树

1.普利姆(prim)算法

Prim算法: 归并顶点,与边数无关,适于稠密网
设连通网络 N = { V, E }
♦ 从某顶点 u0 出发,选择与它关联的具有最小权值的边(u0, v),将其顶点加入到生成树的顶点集合U中
♦ 每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u, v),把它的顶点加入到U中
♦ 直到所有顶点都加入到生成树顶点集合U中为止
在这里插入图片描述

注意:在归并点的过程中不能形成环

2.克鲁斯尔(kruskal)算法

Kruskal算法:归并边,适于稀疏网
设连通网络 N = { V, E }
1. 构造一个只有 n 个顶点,没有边的非连通图 T = { V,  }, 每个顶点自成一个连通分量
2. 在 E 中选最小权值的边,若该边的两个顶点落在不同的连通分量上,则加入 T 中;否则舍去,重新选择
3. 重复下去,直到所有顶点在同一连通分量上为止

在这里插入图片描述

注意:在归并边的过程中不能形成环

四.最短路径

🥳最短路径是指两个顶点之间权值最小的路径(有向图,无向图均适用,不能有负权环)
在这里插入图片描述

(1)最短路径之无权图

无权图相当于各个边权值为1的有权图
在这里插入图片描述

(2)最短路径之负权环

有环且存在负权边即为负权环

在这里插入图片描述

算法实现

1 .Dijkstra(迪杰斯特拉算法)

Dijkstra 属于单源最短路径算法,用于计算一个顶点到其他所有顶点的最短路径
(所谓单源最短路径就是起点是确定的终点是任意一个节点,通过迪杰斯特拉算法可以计算出从起点出发到任意一个节点的最短路径)。
🌀使用前提:不能有负权边
🌀时间复杂度:可优化至 O( ElogV) ,E 是边数量,V 是节点数量
🌀 由荷兰的科学家 Edsger Wybe Dijkstra 发明,曾在1972年获得图灵奖
在这里插入图片描述

算法思想具体形象化(缓慢拉绳子案例)

如下图(a)所示是一张无向图,最短路径的起点设为A。假设一种场景我们将每一个节点想象成一个小石头块。节点之间的边想象成连接石头的绳子。我们将该图平铺在桌子上,如图(b)所示是一张俯视图。黄色的底边是桌子。我们缓缓拉起A石子,紧接着别的石子也陆陆续续被拉起来。等到石头全部被拉上来之后紧绷的绳子就是最短路径。
在图被完全拉起的过程中其实是有路径的更新,一开始紧绷的绳子,在后续石子被拉起来后可能一开始紧绷的绳子后来就变的松弛。
◼ 有一个很关键的信息
✓后离开桌面的小石头
✓ 都是被先离开桌面的小石头拉起来的
在这里插入图片描述在这里插入图片描述

		//每个节点的value对应一条最短路径
    public Map<V, pathInfo<V,E>> digkstra(V begin) {

        Vertex<V,E> beginVertex=vertexes.get(begin);
        if(beginVertex==null)return null;

        //存放目前已经确定的最短路径的边
        Map<V,pathInfo<V,E>> selectedPaths=new HashMap<>();
        Map<Vertex<V,E>,pathInfo<V,E>> paths=new HashMap<>();

        //初始化paths,初始化路径和路径权值和
        for(Edge<V,E>edge:beginVertex.outEdges){
            pathInfo<V,E>path=new pathInfo<>();
            path.weight=edge.weight;
            path.edgeInfos.add(edge.toInfo());
            paths.put(edge.to,path);
        }

        //遍历vertex的出度的边更新路径表
        //找出从该点出发的最小路径
        while(!paths.isEmpty()){
            Map.Entry<Vertex<V,E>,pathInfo<V,E>> min=getMinPath(paths);
            Vertex<V,E> minVertex=min.getKey();
            //获得到节点的最小路径后
            //1.将节点加入到selectedPath中
            //2.继续遍历该节点的outEdges()找到他连接的节点看看是不是需要更新路径
            selectedPaths.put(minVertex.val,min.getValue());
            paths.remove(minVertex);//已经遍历过了z'x
            for(Edge<V,E> edge:minVertex.outEdges){
                if(selectedPaths.containsKey(edge.to)||edge.to.equals(beginVertex))continue;
                relaxFordigkstra(min.getValue(),paths,edge);
            }
        }
         // selectedPaths.remove(begin);//针对无向图的改进方法二
        return selectedPaths;
    }
    public void relaxFordigkstra( pathInfo<V,E> fromPath,Map<Vertex<V,E>,pathInfo<V,E>> paths,Edge<V,E> edge){
        E newValue=weightManager.add(fromPath.weight,edge.weight);
        //获得路径信息然后间接获得路径的权值
        pathInfo<V,E> oldPath=paths.get(edge.to);
        //压根不需要更新路径
        if(oldPath!=null&&weightManager.compare(newValue,oldPath.weight)>=0)return ;
        //之前没有路径
        if(oldPath==null){
            oldPath=new pathInfo<>();
            paths.put(edge.to,oldPath);
        }else {
            oldPath.edgeInfos.clear();
        }
        //这样安排就是为了可以不用在新new一个pathInfo
        oldPath.weight=newValue;
        oldPath.edgeInfos.addAll(fromPath.edgeInfos);
        paths.put(edge.to,oldPath);
    }

由于代码的关联性很强,所以理解起来很困难,可以到的我的代码仓库查看详细代码。gitee代码仓库(点这里呦~)

2. Bellman-Ford(贝尔曼-福特算法)

Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环
💖💖算法原理:对每一条边进行 V – 1 次松弛操作( V 是节点数量),得到所有可能的最短路径
💖💖时间复杂度:O (EV) ,E 是边数量,V 是节点数量
下图的最好情况是恰好从左到右的顺序对边进行松弛操作
对所有边仅需进行 1 次松弛操作就能计算出A到达其他所有顶点的最短路径
在这里插入图片描述 最坏情况是恰好每次都从右到左的顺序对边进行松弛操作
对所有边需进行 V – 1 次松弛操作才能计算出A到达其他所有顶点的最短路径

在这里插入图片描述

public void relaxForbellmanFord(pathInfo<V,E> fromPath,Map<V,pathInfo<V,E>> paths,Edge<V,E> edge){
    E newValue=weightManager.add(fromPath.weight,edge.weight);
    //获得路径信息然后间接获得路径的权值
    pathInfo<V,E> oldPath=paths.get(edge.to.val);
    //压根不需要更新路径
    if(oldPath!=null&&weightManager.compare(newValue,oldPath.weight)>=0)return ;
    //之前没有路径
    if(oldPath==null){
        oldPath=new pathInfo<>();
        paths.put(edge.to.val,oldPath);
    }else {
        oldPath.edgeInfos.clear();
    }
    //这样安排就是为了可以不用在新new一个pathInfo
    oldPath.weight=newValue;
    oldPath.edgeInfos.addAll(fromPath.edgeInfos);
    paths.put(edge.to.val,oldPath);
}
 public Map<V,pathInfo<V,E>> bellmanFord(V begin){
        Vertex<V,E> beginVertex=vertexes.get(begin);
        if(beginVertex==null) return null;
        Map<V,pathInfo<V,E>> selectedPath=new HashMap<>();
        pathInfo<V,E> beginPath=new pathInfo<>();
        beginPath.weight=weightManager.zero();
        selectedPath.put(begin,beginPath);

        int count=vertexes.size()-1;
        for(int i=0;i<count;i++){
            for(Edge edge:edges){
               pathInfo<V,E> fromPath=selectedPath.get(edge.from.val);
               //fromPath可能为Null,松弛失败->前一个节点的最短路径还没有确定
                //selectedPath本身就是空的,所以要一开始就添加一个意义上是A->A的路径,但是不能直接添加字符串因为会造成A->A->b的现象
                //所以要定义一个意义上为0的权值概念,但是权值是泛型,所以我在接口添加zero方法用来解释不通类型0权值的含义
               if(fromPath==null)continue;
               relaxForbellmanFord(fromPath,selectedPath,edge);
            }
        }
        return selectedPath;

    }

五.拓扑排序

1.AOV网(Activity On Vertex Network)

一项大的工程常被分为多个小的子工程
✓ 子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始
◼ 在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)
✓ 以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网
◼ 标准的AOV网必须是一个有向无环图(Directed Acyclic Graph,简称 DAG)

2.拓扑排序原理

◼ 前驱活动:有向边起点的活动称为终点的前驱活动
只有当一个活动的前驱全部都完成后,这个活动才能进行
◼ 后继活动:有向边终点的活动称为起点的后继活动
在这里插入图片描述
◼ 什么是拓扑排序?
将 AOV 网中所有活动排成一个序列,使得每个活动的前驱活动都排在该活动的前面
比如上图的拓扑排序结果是:A、B、C、D、E、F 或者 A、B、D、C、E、F (结果并不一定是唯一的)

Override
    public List<V> topologicalSort() {
        List<V> list=new ArrayList<>();
        Queue<Vertex<V,E>> queue=new LinkedList();
        //将一开始为度不为零的节点与同他它的入度形成映射
        Map<Vertex<V,E>,Integer> map=new HashMap<>();
        //初始化表:入度为0的都放入队列,入度不为0的放入表中等待更新
        vertexes.forEach((V v,Vertex<V,E> ver)->{
           int ins=ver.inEdges.size();
           if(ins==0){
               queue.offer(ver);
           }else{
               map.put(ver,ins);
           }
        });
        while(!queue.isEmpty()){
            Vertex<V,E> vertex=queue.poll();
            //加入到输出表中
            list.add(vertex.val);
            for(Edge<V,E> edge:vertex.outEdges)
            {
                //原本节点的入度减一
                int in=map.get(edge.to)-1;
                //如果入度减一后变为度为0
                if(in==0){
                    queue.offer(edge.to);
                }else{
                    //入过不为0则更新入度表
                    map.put(edge.to,in);
                }
            }
        }

        return list;
    }

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129186.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第十一天(栈与队列)| 20. 有效的括号,1047. 删除字符串中的所有相邻重复项,150. 逆波兰表达式求值

代码随想录算法训练营第十一天&#xff08;栈与队列&#xff09;| 20. 有效的括号&#xff0c;1047. 删除字符串中的所有相邻重复项&#xff0c;150. 逆波兰表达式求值 20. 有效的括号 讲完了栈实现队列&#xff0c;队列实现栈&#xff0c;接下来就是栈的经典应用了。 大家先…

激发原创力量,逐梦数智未来 | 麒麟信安闪耀openEuler Summit 2022

[中国&#xff0c;北京&#xff0c;2022年12月29日]由欧拉开源社区发起并联合华为、麒麟信安、麒麟软件、统信软件等伙伴&#xff0c;共同举办的openEuler Summit 2022于线上举行。 会上&#xff0c;开放原子开源基金会宣布openEuler项目群成立&#xff1b;同时&#xff0c;新…

CVPR 2021|Deep-SfM-Revisited:DeepLearn+经典SfM流程

&#x1f3c6;前言&#xff1a;文章回顾了深度学习在SfM中的应用&#xff0c;并提出了一种新的深度两视图SfM框架。该框架结合了深度学习和经典SfM几何算法的优点。在各个数据集上取得较好的结果。 文章目录解决的问题基本流程实施细节光流估计相机位姿估计尺度不变的深度估计损…

关于安科瑞电力监控系统在电力工程中的应用与对策

摘要&#xff1a;随着经济的发展&#xff0c;能源消耗速度正不断加快&#xff0c;因此我国提出了绿色可持续发展战略&#xff0c;要求在发展建设中以节能为主&#xff0c;不断减少资源能耗&#xff0c;而电能便是首要控制内容。如今我国为电能使用&#xff0c;对计量表进行了优…

《PyTorch深度学习实践》完结合集-P4反向传播作业

二次函数的反向传播 代码&#xff1a; import torch x_data [1.0, 2.0, 3.0] y_data [2.0, 4.0, 6.0] w1 torch.Tensor([1.0]) w1.requires_grad True #定义要进行反向传播 w2 torch.Tensor([1.0]) w2.requires_grad True b torch.Tensor([1.0]) b.requires_grad T…

PHP结课报告-简单的点菜系统(完整版,附源码与数据库)

1 系统背景及意义 1.1系统开发背景介绍 餐饮业在我国有着悠久的发展历史&#xff0c;是与人民生活密切相关的 行业之一&#xff0c;同时也是我国经济增长的新亮点&#xff0c;在 2010 年全国餐饮零 售总额高达 15913 亿元&#xff0c;实际增长约 14.3。在信息系统相对普及的 今…

项目经理通过甘特图编制项目计划的方法

项目有个特征叫 渐进明晰 &#xff0c;指一个项目不可能一开始就能想到最后交付成果的全部细节&#xff0c;项目最终的交付成果到底长啥样是随着项目的推进慢慢变得清晰的&#xff0c;而这个变化的过程不仅需要项目经理的经验与能力&#xff0c;还需要借助项目管理工具的帮助。…

一文讲清「敏捷路线图」,从此不再掉入瀑布陷阱

尽管许多组织和团队声称自己非常敏捷&#xff0c;但他们仍在使用瀑布的方式规划产品。为什么会这样&#xff1f;我们该如何改变这种「错误敏捷」&#xff1f; 原则上&#xff0c;践行敏捷开发很简单&#xff1a;构建一个增量&#xff1b;测试这个增量&#xff1b;了解需要改变…

webdriver的尝试:四 【移动端的使用appium-定位与获取代码】

文章目录界面的简单认识和使用参考我们前面使用webdriver可以实现&#xff1a;使用代码&#xff0c;自动化打开浏览器及指定页面&#xff0c;定位元素&#xff0c;获取内容或者模仿操作那么使用移动端&#xff0c;依然需要实现定位与操作 主要就是在appium inspector上进行操作…

开源全球公司贡献 49 名,涛思数据荣登 2022 中国开发者影响力年度榜单

12 月 28 日&#xff0c;CSDN 在北京举行 “2022 中国开发者影响力盛典暨 CSDN 企业生态汇”活动&#xff0c;同时正式发布 2022 中国开发者影响力年度榜单。本次公布的榜单主要包括年度开源贡献企业、年度开源影响力项目、年度开发者社区、年度数字化创新企业、年度优秀雇主企…

蓝桥杯Python练习题13-圆的面积

资源限制   内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述   给定圆的半径r&#xff0c;求圆的面积。 输入格式   输入包含一个整数r&#xff0c;表示圆的半径。 输出格式   输出…

自然语言处理复习

目录 第 3 章 深度学习基础 第 4 章 语言模型词向量 第 5 章 NLP中的注意力机制 第 6 章 NLP基础任务 第 7 章 预训练语言模型 第 3 章 深度学习基础 1. 人工神经网络 2. 激活函数 &#xff08;1&#xff09;激活函数的作用 为了增强网络的表达能力&#xff0c;需要引入…

在你的岗位中领导:为人力资源职员提供提升招聘管理能力

香港 (Xinwengao.com) — 每年外籍老师表示他们因为沟通问题停止考虑某间学校。这会发生有几个原因但大部分则是因为前线员工并没有感受到得到可以维持与候选人互动。 PD Academia 在此提供这个双语培训课程来帮助中国的学校提升招聘管理能力&#xff1a; 在你的岗位中领导&a…

数字藏品年终盘点:从长坡厚雪到无人问津,潮起潮落的2022

文/尹宁 出品/陀螺研究院 数字藏品&#xff0c;无疑是2022年我国区块链界最为瞩目的应用&#xff0c;在这一年中&#xff0c;数字藏品行业从高歌猛进到急速刹车、从企业扎堆到巨头退场&#xff0c;从需求高涨到被迫出清&#xff0c;经历了跌宕起伏、高潮迭起的发展历程。 上…

ARM64内存虚拟化分析(8)coalesed MMIO处理

从前面MMIO的处理可以看到&#xff0c;每次访问MMIO都会导致虚拟机退了到QEMU中。很多时候多个MMIO操作&#xff0c;这个时候可以先将前面的MMIO操作保存起来&#xff0c;等到最后一个MMIO的时候&#xff0c;再一起退出到QEMU中处理&#xff0c;这就是coalesced MMIO。目前只支…

新概念1 课文和单词(2022/12/28)

前言: 学了多年的英语&#xff0c;在交流或写作中或多或少有用错的地方。估计看这篇博客的你&#xff0c;或许也和我遇到同样的问题。 为此找新概念系列书籍4本&#xff0c;作为系统性查漏补缺的工具。每天更新10课内容&#xff0c;从基础到提升。从简单到深入&#xff0c;循序…

CSS 特殊字符 ‘#‘ 、‘.’ 、‘*’、‘:’、空格“ ”、 ‘>’ , ‘,’

# 号&#xff1a; #号的作用是对应html中的标签的id属性&#xff0c;写法为#name。如#p1{...}会给<p id"p1">ID is p1</p>增加样式。具体事例如下&#xff1a; HTML代码&#xff1a; <div id"p1"> </div><!-- CSS代码&#…

How to use template

1. 模板变量 注意&#xff1a;在manage.py路径下启动shell My name is hjb 我们可以调用Template类中的render方法来对模板进行一个渲染&#xff0c;这时需要用一个Context来传递它&#xff0c;这个Context是一系列变量和他们值得一个集合得字典 首先建立&#xff0c;然后再vie…

IT运维工单高效协同,助力打造一站式运维方案

随着经济全球化的发展趋势&#xff0c;信息系统在企业运营中占据着愈发重要的位置。业务系统越来越多&#xff0c;用户对信息系统的依赖性越来越强&#xff0c;关键业务系统的中断都将导致企业业务、服务的中断&#xff0c;极大的影响了企业业务稳定运行和持续发展。因此企业需…

使用Visual Studio Installer打包Unity的exe应用

前言 Unity在PC端更新软件的方法。 一、下图是Unity发布出来的应用 二、安装Microsoft Visual Studio Installer Projects插件 &#xff08;1&#xff09;打开vs&#xff0c;我这里用的是vs2019 &#xff08;2&#xff09;点击扩展->管理扩展&#xff0c;如果没有安装过&…