spfa之求最短路和判断负环

题目 spfa求最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 impossible。

数据保证不存在负权回路。

输入格式
第一行包含整数 n和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式
输出一个整数,表示 1号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible

数据范围
1 ≤ n,m ≤ 105,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2


spfa 算法思路

明确一下松弛的概念。

  1. 考虑节点u以及它的邻居v,从起点跑到v有好多跑法,有的跑法经过u,有的不经过。

  2. 经过u的跑法的距离就是distu+u到v的距离。

  3. 所谓松弛操作,就是看一看distv和distu+u到v的距离哪个大一点。

  4. 如果前者大一点,就说明当前的不是最短路,就要赋值为后者,这就叫做松弛。

spfa算法文字说明:

  1. 建立一个队列,初始时队列里只有起始点。

  2. 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。

  3. 再建立一个数组,标记点是否在队列中。

  4. 队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。

  5. 重复执行直到队列为空。

  6. 在保存最短路径的数组中,就得到了最短路径。

spfa 图解:

  1. 给定一个有向图,如下,求A~E的最短路。

在这里插入图片描述

  1. 源点A首先入队,然后A出队,计算出到BC的距离会变短,更新距离数组,BC没在队列中,BC入队
    在这里插入图片描述

  2. B出队,计算出到D的距离变短,更新距离数组,D没在队列中,D入队。然后C出队,无点可更新。
    在这里插入图片描述

  3. D出队,计算出到E的距离变短,更新距离数组,E没在队列中,E入队。
    在这里插入图片描述

  4. E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8
    在这里插入图片描述

代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列

void add(int a, int b, int c){//图中添加边和边的端点
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa(){
    q[++tt] = 1;//从1号顶点开始松弛,1号顶点入队
    dist[1] = 0;//1号到1号的距离为 0
    st[1] = 1;//1号顶点在队列中
    while(tt >= hh){//不断进行松弛
        int a = q[hh++];//取对头记作a,进行松弛
        st[a] = 0;//取完队头后,a不在队列中了
        for(int i = h[a]; i != -1; i = ne[i])//遍历所有和a相连的点
        {
            int b = e[i], c = w[i];//获得和a相连的点和边
            if(dist[b] > dist[a] + c){//如果可以距离变得更短,则更新距离

                dist[b] = dist[a] + c;//更新距离

                if(!st[b]){//如果没在队列中
                    q[++tt] = b;//入队
                    st[b] = 1;//打标记
                }
            }
        }
    }
}
int main(){
    memset(h, -1, sizeof h);//初始化邻接表
    memset(dist, 0x3f, sizeof dist);//初始化距离
    int n, m;//保存点的数量和边的数量
    cin >> n >> m;
    for(int i = 0; i < m; i++){//读入每条边和边的端点
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);//加入到邻接表
    }
    spfa();
    if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达 
        cout << "impossible";
    else cout << dist[n];//否则能到达,输出距离
    return 0;
}

题目 spfa判断负环

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你判断图中是否存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。

数据范围
1 ≤ n ≤ 2000,
1 ≤ m ≤ 10000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes


算法分析

使用spfa算法解决是否存在负环问题

求负环的常用方法,基于SPFA,一般都用方法 2(该题也是用方法 2):

  1. 方法 1:统计每个点入队的次数,如果某个点入队n次,则说明存在负环
  2. 方法 2:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则也说明存在环

y总的原话
每次做一遍spfa()一定是正确的,但时间复杂度较高,可能会超时。初始时将所有点插入队列中可以按如下方式理解:

在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于视频中的做法了。那么视频中的做法可以找到负环,等价于这次spfa可以找到负环,等价于新图有负环,等价于原图有负环。得证。

  1. dist[x] 记录虚拟源点到x的最短距离

  2. cnt[x] 记录当前x点到虚拟源点最短路的边数,初始每个点到虚拟源点的距离为0,只要他能再走n步,即cnt[x] >= n,则表示该图中一定存在负环,由于从虚拟源点到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用

  3. dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1,往前走一步

注意:该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点
在这里插入图片描述

时间复杂度 一般:O(m) 最坏:O(nm)

Java 代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    static int n;
    static int m;
    static int N = 2010;
    static int M = 10010;
    static int[] h = new int[N];
    static int[] e = new int[M];
    static int[] ne = new int[M];
    static int[] w = new int[M];
    static int idx = 0;
    static int[] dist = new int[N];//记录虚拟点到x的最短距离
    static int[] cnt = new int[N];//从虚拟点到x经过的边数 
    static boolean[] st = new boolean[N];

    public static void add(int a,int b,int c)
    {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx ++;

    }
    public static boolean spfa()
    {
        Queue<Integer> queue = new LinkedList<Integer>();
        //将所有点进入队列
        for(int i = 1;i <= n;i++)
        {
            queue.add(i);
            st[i] = true;
        }
        while(!queue.isEmpty())
        {
            int t = queue.poll();
            st[t] = false;
            for(int i = h[t]; i != -1;i = ne[i])
            {
                int j = e[i];
                if(dist[j] > dist[t] + w[i])
                {
                    dist[j] = dist[t] + w[i];
                    cnt[j] = cnt[t] + 1; 

                    if(cnt[j] >= n) return true;
                    if(!st[j])
                    {
                        queue.add(j);
                        st[j] = true;
                    }


                }
            }
        }
        return false;
    }
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String[] str1 = reader.readLine().split(" ");
        n = Integer.parseInt(str1[0]);
        m = Integer.parseInt(str1[1]);
        Arrays.fill(h, -1);
        while(m -- > 0)
        {
            String[] str2 = reader.readLine().split(" ");
            int a = Integer.parseInt(str2[0]);
            int b = Integer.parseInt(str2[1]);
            int c = Integer.parseInt(str2[2]);
            add(a,b,c);
        }
        if(spfa()) System.out.println("Yes");
        else System.out.println("No");
    }

}
  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值