最短路算法

前要知识

  • n为点数,m为边数
  • 在图论中,无向图其实可以看成是一种特殊的有向图,连一条A->B的边,再连一条B->A的边即可
  • 稠密图用邻接矩阵来存储,稀疏图用邻接表来存储(根据题目数据范围进行判断,当m与\(n^2\)一个级别时,即为稠密图;当m与n一个级别时即为稀疏图)

知识结构

最短路知识结构

朴素Dijkstra算法

Dijkstra的整体思路比较清晰:即进行n(n为点的个数)次迭代去确定每个点到起点的最小值,每次迭代中确定一个点,最后输出的终点即为我们要找的最短路的距离。

Dijkstra算法是基于贪心的,有关算法的具体解释可以参考这一篇博客:图最短路径算法之迪杰斯特拉算法(Dijkstra)

题目

AcWing849

思路

对于重边和自环的处理:由于边权值都是正数,所以哪怕存在自环,也不会对结果产生影响,对于重边的处理,可以在输入时进行判断,取最小值即可。

代码

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

const int N = 510;

int g[N][N];    // 为稠密阵所以用邻接矩阵存储
int dist[N];    // 用于记录每一个点距离第一个点的距离
bool st[N];     // 用于记录该点的最短距离是否已经确定

int n,m;

int Dijkstra()
{
    memset(dist, 0x3f,sizeof dist);     // 初始化距离,0x3f代表无限大

    dist[1] = 0;  // 第一个点到自身的距离为0

    for(int i = 0; i < n; i++)      // 有n个点所以要进行n次迭代
    {
        int t = -1;       // t存储当前访问的点

        for(int j = 1; j <= n; j++)   //这里的j代表的是从1号点开始
            if(!st[j] && (t == -1 || dist[t] > dist[j]))  // 找到当前距离最小的点加入确认点集合中    
                t = j;

        st[t] = true;   

        for(int j = 1;j <= n; j++)           //依次更新每个点所到相邻的点路径值
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }

    if(dist[n] == 0x3f3f3f3f) return -1;  //如果第n个点路径为无穷大即不存在最低路径
    return dist[n];
}
int main()
{
    cin >> n >> m;

    memset(g, 0x3f, sizeof g);    //初始化图 因为是求最短路径,所以每个点初始为无限大

    while(m--)
    {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z);     //如果发生重边的情况则保留最短的一条边
    }

    cout << Dijkstra() << endl;
    return 0;
}

堆优化版Dijkstra算法

题目

AcWing850

思路

参考这俩位大佬的解法笔记:

  1. 小呆呆
  2. optimjie

代码

#include<iostream>
#include<cstring>
#include<queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 100010; // 把N改为150010就能ac

// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定

int n, m;

void add(int x, int y, int c)
{
    w[idx] = c; // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
    e[idx] = y; // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
    ne[idx] = h[x]; // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
    h[x] = idx++;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
    // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时    
    // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
    heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
    while(heap.size())
    {
        PII k = heap.top(); // 取不在集合S中距离最短的点
        heap.pop();
        int ver = k.second, distance = k.first;

        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
            if(dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({ dist[j], j });
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);

    while (m--)
    {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        add(x, y, c);
    }

    cout << dijkstra() << endl;

    return 0;
}

Bellman_Ford算法

题目

AcWing853

思路

  • 该算法适合处理带有负权边的题目,尤其是带有最多经过k条边这样条件的题目

  • 除此之外,该算法还可以判断图中是否存在负环,具体判断操作如下:

    算法经过\(V - 1\)次松弛迭代过后,如果从源点到各点距离还存在变化,那就说明必定存在负环

  • 算法最多经过k条边如何在代码中进行体现?只需要让最外层循环次数为k即可!经过模拟,个人认为这个算法的松弛过程有点类似于BFS的逐步扩散过程

  • 在每一次松弛过程中,需要先让先前一次迭代过程后的距离数组备份,防止出现串联现象!

可以参考的题解和博客:

  1. 松鼠爱葡萄
  2. 数据结构(十一):最短路径(Bellman-Ford算法)

代码

#include <iostream>
#include <cstring>

using namespace std;
const int N = 1e4 + 10;
int n, m, k;
int dist[N], backup[N];	// 注意定义备份距离数组

// 直接开个结构体来存边
struct
{
    int a, b, w;
}edges[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for(int i = 0; i < k; i++)  // 松弛k轮
    {
        memcpy(dist, backup, sizeof dist);	// 备份
        for(int i = 0; i < m; i++)	
        {
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            if(dist[b] > backup[a] + w)	// 迭代松弛
                dist[b] = min(dist[b], backup[a] + w);
        }
    }
    // 注意这里为什么是 0x3f3f3f3f / 2 ? 
    // 因为总共k次松弛,假设某个点一直被它周围的某个点松弛(负权边)并且负权边最小为-10000
    // 并且k最大为500,那么最多会减少5000000,还是满足大于0x3f3f3f3f / 2
    if(dist[n] > 0x3f3f3f3f / 2) cout << "impossible";
    else cout << dist[n]; 
}

int main()
{
    cin >> n >> m >> k;
    for(int i = 0; i < m; i++)
    {
        int x, y, w;
        cin >> x >> y >> w;
        edges[i] = {x, y, w};
    }
    bellman_ford();
    return 0;
}

SPFA算法

题目

思路

我们注意到Bellman_ford每次都会遍历所有的边来进行更新操作,但是其实我们可以注意到很多遍历其实都没有什么用,因此spfa针对这一步进行了优化,创建一个队列来加入每次距离被更新的结点。

注意:

  • st数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。

  • Bellman_ford算法里最后的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。

  • Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。总结来说,Bellman_ford可以处理任意带负权边和负权环的图,SPFA可以处理带负权边的图,Dijkstra只能处理带正权边的图。

可以参考的题解和博客:

  1. AcWing 851. SPFA算法

代码

#include<bits/stdc++.h>
#define xx first
#define yy second
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const bool MODE = 1;
const int INF = 0x3f3f3f3f, N = 1e5 + 10;
int n, m;
int dist[N];
bool st[N];
int h[N], e[N], ne[N], idx, w[N];
inline void add(int a, int b, int c)
{
    w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void spfa()
{
    queue<int> q;
    dist[1] = 0;
    st[1] = true;
    q.push(1);
    while (q.size())
    {
        auto t = q.front();
        q.pop();
        st[t] = false;  // 从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])  // 当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可
                {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    if (dist[n] == INF) cout << "impossible" << endl;
    else cout << dist[n] << endl;
}
void solve()
{
    memset(dist, 0x3f, sizeof dist);
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < m; ++i)
    {
        int a, b, c; cin >> a >> b >> c;
        add(a, b, c);
    }
    spfa();
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    if (MODE) freopen("Debug/input.txt", "r", stdin), freopen("Debug/output.txt", "w", stdout);
    solve();
    if (MODE) fclose(stdin), fclose(stdout);
    return 0;
}

Floyd算法

题目

思路

Floyd算法的核心是基于动态规划,并且经过了滚动数组进行优化。

分析过程如下图所示:

可以参考的题解和博客:

  1. 图最短路径算法之弗洛伊德算法(Floyd)
  2. AcWing 854. Floyd闫式dp分析法

代码

#include<bits/stdc++.h>

#define xx first
#define yy second
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const bool MODE = 0;
const int INF = 0x3f3f3f3f, N = 210;
int dist[N][N], p[N][N];  // p[i][j]代表i-j两点之间最短路径必须经过p[i][j]表示的点
int n, m, t;

void floyd()
{
    for (int k = 1; k <= n; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                if (dist[i][j] > dist[i][k] + dist[k][j])
                {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    p[i][j] = k;  // i-j之间必须经过k
                }
}

string get_path(int u, int v)
{
    if (p[u][v] == -1) return to_string(u) + "->"; // base case: 直接相连的情况
    int k = p[u][v];
    return get_path(u, k) + get_path(k, v); // 返回左右两端u->k以及k->v的路径
}

void solve()
{
    cin >> n >> m >> t;
    memset(dist, 0x3f, sizeof dist);
    memset(p, 0x3f, sizeof p);
    for (int i = 1; i <= n; ++i) dist[i][i] = 0;  // 处理自环
    for (int i = 0; i < m; ++i)
    {
        int a, b, c;
        cin >> a >> b >> c;
        dist[a][b] = min(dist[a][b], c);  // 处理重边
        p[a][b] = -1;  // 如果两点之间有一条直接路径,那么意味着无需经过任何点,我们设为-1
    }
    floyd();
    while (t--)
    {
        int x, y;
        cin >> x >> y;
        if (dist[x][y] > INF / 2) cout << "impossible" << endl;  // 由于循环过程可能存在负权边更新,所以必须要大于inf/2
        else
        {
            cout << dist[x][y] << endl;
            string path = get_path(x, y) + to_string(y);
            cout << path << endl;
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    if (MODE) freopen("Debug/input.txt", "r", stdin), freopen("Debug/output.txt", "w", stdout);
    solve();
    if (MODE) fclose(stdin), fclose(stdout);
    return 0;
}
posted @ 2023-02-08 22:41  Pluto_Evans  阅读(25)  评论(0)    收藏  举报