最短路问题


算法适用范围

约定:n是点的数量,m是边的数量
n 2 n^2 n2 与 m 是相同级别的是稠密图
稠密图使用朴素Dijkstra算法O( n 2 n^2 n2),若使用堆优化的就是 n 2 l o g n n^2logn n2logn

n, m < 10 e 5 10e5 10e5 时使用堆优化即可
有向图与无向图使用算法是没有区别的

在这里插入图片描述

  • 单源最短路指从一个点到其他点的最短路
  • 多源汇最短路指可能有多个询问问任意两点的最短路
  • 自环的处理方法是将它初始化为零
  • 重边就是保留最小的边

提示:例题中的注释是精华

朴素的Dijkstra算法

实现步骤
s:当前已确定最短距离的点

  1. dist[i] = 0, dist[i] = ∞ \infty
  2. for (int i = 0; i < n; i ++)
  3. 找到不在s中的距离最近的点
  4. t加到s中
  5. 用t更新其他点的距离

模板
稠密图用邻接矩阵来存

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

时间复杂是 O( n 2 n^2 n2 + m)

例题
AcWing 849. Dijkstra求最短路 I

/*
    Dijkstra算法 是将初始点初始为零,其他所有点初始为正无穷
    首先是在所有点中寻找到源点距离最短的点,由于距离源点较远的点未被遍历所以距离时正无穷,符合从根源开始找的人类思维
    而找到这个最小的点就说明从源点到这个点是最优的,那么更新从源点到这个点相邻的点(使其到源点距离最短)g[t][j] != 0x3f3f3f说明连通,更新从t点走的距离如果更短就更新
    如果在经历了n - 1次(最后一次集合只剩1个点无需遍历直接拿出)相当于每个结点都有找到的机会,如果这样还为找到说明无最短路
*/

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;

int g[N][N], dist[N];// g[i][j]表示结点i和结点j的边的权重, dist[i] 表示第i个节点从源点到它的最短距离
bool st[N]; // st[N] 表示已经确定的最短距离了的结点
int n, m;

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++)
    {
        int t = -1; // 初始化开始是的确定最短距离了的结点
        for (int j = 1; j <= n; j ++)
            if (!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
        st[t] = true;// 找到下一个要走的结点,它到源点的距离最短

        for (int j = 1; j <= n; j ++)
            dist[j] = min(dist[j], dist[t] + g[t][j]);// 如果从t走时到j的距离更小,就走t,否则有其他的路, 当未连通时 0x3f3f3f 一定小于 0x3f3f3f 加一个数仍未连通
    }
    if (dist[n] > 0x3f3f3f >> 1) return -1; // 最后dist[n]的距离可能会改变,但不会变太多
    else return dist[n];
}

int main ()
{
    memset(g, 0x3f, sizeof g);
    cin >> n >> m;
    for (int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c); // 重边的处理办法,只保留最短的边
    }
    printf("%d", dijkstra());
    return 0;
}

堆优化的Dijkstra算法

内容
稀疏图使用邻接表存储

由于朴素的Dijkstra算法中最费时间的是在图中遍历找到距离起点最小的结点的步骤上,所以可以使用堆来存储结点,这样每次就可以以O(1)的时间复杂度找到距离最小的结点

但是,这样的化每次修改邻接的点的距离时就会引起堆结构的变化O(mlogn)的时间复杂度

模板

时间复杂度 n l o g n nlogn nlogn

最小堆写法:priority_queue<PII, vector, greater > heap;

typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
  memset(dist, 0x3f, sizeof dist);
  dist[1] = 0;
  priority_queue<PII, vector<PII>, greater<PII>> heap;
  heap.push({0, 1});      // first存储距离,second存储节点编号

  while (heap.size())
  {
      auto t = heap.top();
      heap.pop();

      int ver = t.second, distance = t.first;

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

      for (int i = h[ver]; i != -1; i = ne[i])
      {
          int j = e[i];
          if (dist[j] > distance + w[i])
          {
              dist[j] = distance + w[i];
              heap.push({dist[j], j});
          }
      }
  }

  if (dist[n] == 0x3f3f3f3f) return -1;
  return dist[n];
}


总结:与写BFS模板类似

例题
AcWing 850. Dijkstra求最短路 II

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

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int h[N], e[N], w[N], ne[N], idx = 0;
int dist[N];
bool st[N];
int n, m;

void add (int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;// 邻接表初始化时记得加入权重参数
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});// first表示距离, second表示节点编号,这是因为在优先队列中是优先按元祖第一个元素进行排序

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();// 记得踢掉,不然循环听不了

        int ver = t.second, distance = t.first;// ver表示节点编号

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

        for (int i = h[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])// 因为要遍历Ver相连的所有边i所以提前将源点到ver的最短距离记作distance, 而w[i]记录的是第i个节点到j的距离(权重)i是与ver相连的边 
            // 将与ver相连的边更新为最短路径值,j是i的下一条边是一个指针关系
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }

        }
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main ()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++)// 注意是输入边的次数,呜呜
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add (a, b, c);
    }
    printf("%d", dijkstra());
    return 0;
}

Bellman-Ford算法

处理有负权边的图
但是不能存在负环,最短路径不一定存在
如果第n次迭代还存在更新说明存在负环

有边数限制的题目只能用Bellman-Ford算法

实现步骤

  1. 迭代k次
    迭代k次指不超过k条边的最短距离
  2. for 循环所有的边 a -> b(w) (a 到 b 权重为w)
  3. 更新最短路径 dist[b] = min(dist[b], dist[a + w]) // 松弛操作
    更新完之后满足 dist[b] <= dist[a] + w

模板

时间复杂度:O(nm)

当存在次数限制时就需要进行备份,一次迭代的过程中会遍历所有边,这会导致最小值的变化,而对下次更新造成影响

//注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1; // 因为无穷可能被负权边更新所以用大于进行判断不能用等于
    
    return dist[n];
}

例题
AcWing 853. 有边数限制的最短路

/*
    bellman_ford算法从每条边出发来对结点进行优化,利用三角不等式进行松弛操作
    每轮都要对所有边进行松弛操作,每轮至少确定一个点的最优值,所以最坏情况要进行n - 1轮操作
    当所有边都满足松弛操作时说明找到了最短路径
    bellman_ford算法适用于限制查询次数的情况下,其他情况都逊于spaf算法
*/
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

struct Edge
{
    int a, b, c;
}edges[M];// bellman_ford算法是将边作为基本点,去找结点所以存储的是结点的信息
int dist[N]; // 同其他几个算法一样表示到源点的最短路径值
int last[N];// 由于这道题中限制了次数所以要进行备份`超出次数的可行方案`(优化的结点路径,但是这个次数超了,要回复到这个操作之前的值)
int n, m, k;// k是限制的次数

int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < k; i ++ )// 呜呜,限制了次数,要循环k次
    {
        memcpy(last, dist, sizeof dist);

        for (int j = 0; j < m; j ++)
        {
            int a = edges[j].a, b = edges[j].b, c = edges[j].c;// 更新经过这条边的后面的结点用前面结点后权重
            if (dist[b] > last[a] + c)
                dist[b] = last[a] + c;
        }
    }
    return dist[n];
}

int main()
{
    cin >> n >> m >> k;

    for (int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }

    bellman_ford();
    if(dist[n] > 0x3f3f3f3f / 2) puts("impossible"); // 可能将dist[n] 中的值改变但是改变的幅度不会太大
    else printf("%d", dist[n]);
    return 0;
}

SPFA算法

每一次不一定都会对dist[b]进行更新只有当dist[a]变小时才会进行更新,因此用bfs 进行优化,将发生变下的结点加入队列

实现步骤

  • 现将起点加入队列
  • while (queue不空)
  1. f <- q.front()
  2. q.pop()
  3. 更新t的所有出边,如果更新成功的话加入队列

模板

时间复杂度平均情况是O(m),最坏情况是O(nm)

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中,防止存重复的点

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        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];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

负环的判断

设置dist[x]最短距离
设置cnt[x] 到最短路的边数
每次更新最短距离时:

  • dist[x] = dist[t] + w[i]
  • cnt[x] = cnt[t] + 1

例题

求最短路

AcWing 851. spfa求最短路

  /*
    spfa算法相当于是将bellman_ford算法的改良用队列来进行维护每条边
    因此邻接表中存储的是边的信息
    算法与Dijkstra算法类似
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int h[N], w[N], e[N], ne[N], idx = 0;// 邻接表用来存边的信息
int dist[N];
bool st[N]; // 区别是st[]数组判断的是这条边是否用过
int n, m;

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

void spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = 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])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}

int main ()
{
    memset (h, -1, sizeof h);
    cin >> n >> m;

    for (int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    spfa();
    if(dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d", dist[n]);
    return 0;
} 

判断负环

AcWing 852. spfa判断负环

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

using namespace std;

const int N = 2010, M = 10010;

int h[N], e[M], w[M], ne[M], idx = 0;
int dist[N], cnt[N];
bool st[N];
int n, m;

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

bool spfa()
{   
 queue<int> q;
 // 由于本体是求是否存在负环,那么就不能这从1开始遍历,因为1可能在负环的后面
 for (int i = 1; i <= n; i ++)
 {
     st[i] = true;// 因此将所有节点都加入到队列中进行遍历
     q.push(i);
 }

 while(q.size())
 {
     auto t = q.front();
     q.pop();

     st[t] = 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];
             cnt[j] = cnt[t] + 1;// 每走一次就加一次,最多也只会走n次因为有n个节点

             if (cnt[j] >= n) return true;// 当大于n是说明存在自环
             if (!st[j])
             {
                 q.push(j);
                 st[j] = true;
             }
         }
     }
 }
 return false;
}

int main()
{
 memset(h, -1, sizeof h);
 cin >> n >> m;
 for (int i = 0; i < m; i ++)
 {
     int a, b, c;
     scanf("%d%d%d", &a, &b, &c);
     add(a, b, c);
 }
 puts(spfa() ? "Yes" : "No");

 return 0;
}

Floyd算法

实现多源汇最短路
算法也可以处理负权边

实现步骤
用邻接矩阵存储所有的边

  • for (int k = 1; k <= n; k ++)
  • for(int i = 1; i <= n ; i ++)
  • for(int j = 1; j <= n; j ++)
    d(i, j) = min (d(i, j), d(i, k) + d(k, j))

模板

时间复杂度是O( n 3 n^3 n3)

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

例题
AcWing 854. Floyd求最短路

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;

int d[N][N]; // d[i][j][k] 表示从i到j必须经过k的最短路径的值
int n, m, q;

void floyd()
{
    for (int k = 1; k <= n; k ++)//k 要在最外层
        for (int i = 1; i <= n; i ++)
            for (int j = 1; j <= n; j ++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);// 简记:k是进过的点所以在中间
}

int main()
{
    cin >> n >> m >> q;
    // 初始化 => memset
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++)
        {
            if (i == j)d[i][j] = 0;// 干掉自环
            else d[i][j] = INF;
        }

    for (int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);// 干掉重边
    }
    floyd();

    while (q --)
    {
        int start, end;
        scanf("%d%d", &start, &end);

        if(d[start][end] > INF / 2) puts("impossible");
        else printf("%d\n", d[start][end]);
    }
    return 0;
}

参考文献

学习自AcWing

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ˇasushiro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值