bellman-ford,spfa + 打印最短路径

本文详细介绍了Bellman-Ford算法及其优化版本SPFA,用于解决单源最短路径问题。Bellman-Ford算法能检测负权边和负权环,而SPFA通过队列优化降低了冗余计算,适用于可能存在负权的情况。文章提供了两种算法的代码实现,并解释了它们的运行流程和优缺点。
摘要由CSDN通过智能技术生成

bellman-ford

Bellman-Ford算法优点

Bellman-Ford算法是用来解决单源最短路问题的。
在现实生活旅游途中,我们通常想知道一个景点到其他所有景点的最短距离,以方便我们决定去哪些比较近的景点。而这时候,Bellman-Ford算法就有用了。

Bellman-Ford算法的优点是可以发现负圈,缺点是时间复杂度比Dijkstra算法高。
而SPFA算法是使用队列优化的Bellman-Ford版本,其在时间复杂度和编程难度上都比其他算法有优势。

虽然题目规定了不存在「负权边」,但我们仍然可以使用可以在「负权图中求最短路」的 Bellman Ford 进行求解,该算法也是「单源最短路」算法,复杂度为 O(VE)。

通常为了确保O(VE) ,可以单独建一个类代表边,将所有边存入集合中,在n次松弛操作中直接对边集合进行遍历(代码见p1 )。

由于本题边的数量级大于点的数量级,因此也能够继续使用「邻接表」的方式进行边的遍历,遍历所有边的复杂度的下界为 O(V),上界可以确保不超过 O(E)(代码见 p2)

算法流程

(1)初始化:将除起点s外所有顶点的距离数组置无穷大 dist[vs] = N, dist[vs] = 0,prev[] = -1;
(2)迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
(3)判断负圈:如果迭代超过V-1次,则存在负圈

代码实现

(1) 类实现

class Edge {
    int a;
    int b;
    int c;

    public Edge(int a, int b, int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}

//n为顶点数量,N为Interger.MAX_VALUE
public static void bellmanClass(int vs) {
        List<Edge> edges = new ArrayList<>();
        int[] prev = new int[n];
        int dist[] = new int[n];
        Arrays.fill(dist, N);
        Arrays.fill(prev, -1);
        dist[vs] = 0;
//改为类存储
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j || matrix[i][j] == N) continue;
                edges.add(new Edge(i, j, matrix[i][j]));
            }
        }

        for (int i = 1; i < n; i++) {
            boolean hasChange = false;
            for (Edge edge : edges) {
                int a = edge.a, b = edge.b, c = edge.c;
                if (dist[a] == N) continue;
                if (dist[b] > dist[a] + c) {
                    dist[b] = dist[a] + c;
                    prev[b] = a;
                    hasChange = true;
                }
            }
            if (!hasChange) break;
        }

//判断负环
        boolean hasRing = false;
        for (Edge edge : edges) {
            int a = edge.a, b = edge.b, c = edge.c;
            if (dist[a] == N) continue;
            if (dist[b] > dist[a] + c) {
                hasRing = true;
                System.out.println("存在负环");
                return;
            }
        }

        // 打印bellman-ford最短路径的结果
        System.out.printf("bellman(%c): \n", vexs[vs]);
        for (int i = 0; i < vexs.length; i++) {
            System.out.printf("  shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
            StringBuilder sb = new StringBuilder();
            sb.append(i);
            for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
                sb.append(">--");
                sb.append(prev[j]);
            }
            System.out.println(sb.reverse());
        }
    }

(2) 邻接链表实现

   private static int[] head = new int[n];

    //edge
    private static int[] to = new int[edgNum];
    private static int[] w = new int[edgNum];
    private static int[] next = new int[edgNum];

    public static void adjacency() {
        int idx = 0;
        Arrays.fill(head, -1);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                if (matrix[i][j] == N) continue;
                to[idx] = j;
                w[idx] = matrix[i][j];
                next[idx] = head[i];
                head[i] = idx;
                idx++;
            }
        }
    }

 public static void bellman(int vs) {
        adjacency();
        int[] dist = new int[n];
        int[] prev = new int[n];
        Arrays.fill(dist, N);
        Arrays.fill(prev, -1);
        dist[vs] = 0;

        for (int i = 1; i < n; i++) {
            //提前结束
            boolean hasChange = false;
            //遍历边
            for (int j = 0; j < n; j++) {
                for (int a = head[j]; a != -1; a = next[a]) {
                    if (dist[j] == N) continue;
                    int id = to[a];
                    if (dist[id] > w[a] + dist[j]) {
                        dist[id] = w[a] + dist[j];
                        prev[id] = j;
                        hasChange = true;
                    }
                }
            }
            if (!hasChange) break;
        }

        //判断负环
        boolean hasRing = false;
        for (int j = 0; j < n; j++) {
            for (int a = head[j]; a != -1; a = next[a]) {
                if (dist[j] == N) continue;
                int id = to[a];
                if (dist[id] > w[a] + dist[j]) {
                    hasRing = true;
                    System.out.println("存在负环");
                    return;
                }
            }
        }
        // 打印bellman-ford最短路径的结果
        System.out.printf("bellman(%c): \n", vexs[vs]);
        for (int i = 0; i < vexs.length; i++) {
            System.out.printf("  shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
            StringBuilder sb = new StringBuilder();
            sb.append(i);
            for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
                sb.append(">--");
                sb.append(prev[j]);
            }
            System.out.println(sb.reverse());
        }
}

SPFA

SPFA算法是求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(VE)。但算法可以进行若干种优化,提高了效率。
SPFA (shortest path faster algorithm) 是一个单源最短路径算法,与另一个单源最短路算法dijkstra不同的是(什么你还不知道dijkstra?),SPFA可以用来处理含有负权的图,并且也可以判断图中是否存在负权回路。

实际上,SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。SPFA的时间复杂度可以达到,k是每一个节点的平均进队次数,可以证明k不大于2。

算法的思路:
我们用数组dist记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止

我们要知道带有负环的图是没有最短路径的,所以我们在执行算法的时候,要判断图是否带有负环,方法有两种:

  1. 开始算法前,调用拓扑排序进行判断(一般不采用,浪费时间)
  2. 如果某个点进入队列的次数超过N次则存在负环(N为图的顶点数)

SPFA算法流程
首先说一下我们都需要哪些东西:

一个储存节点的队列 queue

一个储存当前从原点到每一个节点的最短路径长度的数组dist[n],prev[n](与dijkstra相同)

一个标记此节点是否在队列中的数组visit[n]

一个记录每个节点入队次数的数组count[n](可选,根据图中是否有负权回路判断)

Step1:初始化

与dijkstra类似,首先需要将dist[]数组中每一个元素初始化为,这个我习惯设成0x3f3f3f3f。其次,将visited中的每一个元素设为。将源点的dist值dist[]设为0,将visited[]设为。

Step2:入队

更新与当前节点(如果是第一次循环就是)相连,且在以为弧尾的边上的节点 的最短距离(也就是dist数组的值),如果dist[] + < dist[],那么dist[] = dist[] + 。(weight是两个节点之间的权值)然后,如果节点不在队列中,就将其入队,并记录每一个节点的入队次数,如果次数大于了节点总数,那么就说明这个图中存在回路。

Step3:重复Step2直至队列为空

代码实现
public static void spfa(int vs) {
        adjacency();
        int[] dist = new int[n];
        int[] prev = new int[n];
        boolean[] visit = new boolean[n];
        int[] count = new int[n];
        Arrays.fill(visit,false);
        Arrays.fill(dist, N);
        Arrays.fill(prev, -1);
        dist[vs] = 0;
        visit[vs] = true;
        Deque<Integer> queue = new LinkedList<>();

        queue.offer(vs);
        while (!queue.isEmpty()) {
            int poll = queue.poll();
            visit[poll] = false;
            for (int j = head[poll]; j != -1; j = next[j]) {
                int id = to[j];
                if (dist[poll] == N) continue;
                if (dist[id] > dist[poll] + w[j]) {
                    if(visit[id]) continue;
                    dist[id] = dist[poll] + w[j];
                    prev[id] = poll;
                    if(++count[id] > n){
                        System.out.println("存在负环");
                        return;
                    }
                    visit[id] = true;
                    queue.offer(id);
                }
            }
        }
        // 打印dijkstra最短路径的结果
        System.out.printf("bellman(%c): \n", vexs[vs]);
        for (int i = 0; i < vexs.length; i++) {
            System.out.printf("  shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
            StringBuilder sb = new StringBuilder();
            sb.append(i);
            for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
                sb.append(">--");
                sb.append(prev[j]);
            }
            System.out.println(sb.reverse());
        }
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值