最短路问题算法模板

最短路问题分类

最短路:多源最短路、单源最短路、边权为1(相同)的最短路问题

单源最短路:以是否有负边权为区分
边权都为正 -> 朴素的 Dijkstra 算法(n ^ 2) 稠密图 --> 邻接矩阵
堆优化的 Dijkstra 算法 稀疏图 --> 邻接表
存在负权边 -> Bellman-Ford 算法(O(k * m)) --> 任意方式存储
spfa 算法(O(m) ~ O(n * m)) --> 邻接表

多源最短路:Floyd 算法 O(n ^ 3)

边权相同的多源、单源最短路可以采用队列来实现

Dijkstra 算法求最短路

Dijkstra 算法用于求解边权为正的最短路问题

朴素版 Dijkstra 算法求最短路

使用朴素版本的 Dijkstra 算法,针对稠密图(m > nlogn),m 为边数,n 为点数
建稠密图:使用 邻接矩阵 存储 ,初始将矩阵全部初始化为正无穷,然后根据题目要求建图,邻接矩阵中保留每条有向边的最短长度 g[i][j] = min(g[i][j], len)

时间复杂度:O(n ^ 2)

循环 n 次,每次确定一个点到起点的最短距离:

  1. 从所有没有确定最短路的点中选择一个距离起点最近的点 t
  2. 使用这个点 t 更新其他点距离起点的最短距离:dist[j] = min(dist[j], dist[t] + g[t][j])

代码易错点:st[1] 不能提前改为 true,只能在循环里改

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N];
int dist[N]; // 从 1 号点走到 i 号点的距离
bool st[N]; // 记录每个点是是否确定了最短路
// 当 dist[i] 对应的 st[i] 为 true 时, 则 dist 里存的就是 i 到起点的最短距离 
int n, m;
int Dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for(int i = 0; i < n; i++) // 循环 n 次, 每次找到一个最短路
    {
        int t = -1;
        // 从所有没有确定最短路的点中选择一个dist 最小的
        for(int j = 1; j <= n; j++)
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        st[t] = true; // 这个点就确定了最短路 
        for(int j = 1; j <= n; j++) // 用 1->t + g[t][j] 更新 1->j 
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    if(dist[n] == 0x3f3f3f3f) return -1; // 没找到
    return dist[n];
}
int main()
{
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    // 因为 memset 是对字节操作, 因此这时 0x3f 对于整数相当于 0x3f3f3f3f
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c); // 保证在有重边的情况下,录入最少的
    }
    cout << Dijkstra() << endl;
    return 0;
}

堆优化版 Dijkstra 算法求最短路

堆优化版的 Dijkstra 算法,针对稀疏图(边较少,m < nlogn)
建稀疏图:使用 邻接表 存储,邻接表存储不用考虑重边和自环对结果的影响

时间复杂度:O(m logn)*

定义一个小根堆堆 priority_queue<PII, vector<PII>, greater<PII>> PII 是 pair<int, int> 的重定义,映射为:(first) 距离 -> 节点编号 (second)

先将起点加入堆,开始循环,并取堆顶元素,拿到编号和距离,起点的 first 肯定是最短距离,遍历此 first 能达到的所有点,并更新他们的 dist 数组,并将这些新得出的距离->点的序列对依次加入堆,取堆顶元素 (此时堆顶元素的 first 就是没有确定最短路的点中,距离最短的那一个,如果没有被访问过的话,就可以将这个元素的 first 变为从起点到 second(序号) 的最短路),然后就重复前面的操作,每次遍历新的最短节点下一步能到达的所有节点,并依据最新确定节点的最短路距离更新这些点的 dist 数组…

最终的 dist[n] 就是从起点到节点 n 的最短距离

代码易错点:

判断是否需要更新的时候, 需要将所有被更新后的点都加入到堆中
if(st[ver]) continue; // 当发现这个点已经被遍历过了, 就直接进行 continue 
st[ver] = true; // 这两句必须有, 不然就可能会超时 
if(distance + w[i] < dist[j]) // 这里是加 `w[i]`, 因为距离和两个点之间的距离有关 w[idx] = c
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 150010;
typedef pair<int, int> PII;
int e[N], ne[N], h[N], idx; // 建邻接表
int w[N]; // 存邻接表边的长度
int dist[N]; // 从 1 号点走到 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;
    idx ++;
}
int Dijkstra()
{
    priority_queue<PII, vector<PII>, greater<PII>> Heap; // 建小堆
    memset(dist, 0x3f, sizeof dist); // 初始化 
    dist[1] = 0;
    Heap.push({0, 1}); // 到起点的距离 --> 节点编号 
    while(Heap.size() > 0)
    {
        auto t = Heap.top();
        Heap.pop();
        int distance = t.first, ver = t.second;
        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;
    else return dist[n];
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c); // 建立有向边 
    }
    cout << Dijkstra() << endl;
    return 0;
}

带负权边的最短路

带负权边的最短路可以使用 Belllman-ford 算法和 spfa 算法来求解。

Bellman-ford 算法求带负权边的最短路

Bellman-ford 算法使用于:带负权边,有边数限制 的最短路
但如果没有边数限制,应选择效率更优的 spfa 算法

时间复杂度:O(k * M),k 为题目限制的边数,M 为边数

从起点开始,循环 k 次,每次遍历每条边,更新距离数组 dist
循环 k 次,所求的 dist 数组的含义为:为从起点开始,经过不超过 k 条边,走到每个点的最短距离

如果迭代了 n 次,第 n 次还更新了的话,说明存在负环边(因为第 n 次如果还更新了的话,就说明一共经过了 n 条边,意味着有 n + 1个点,但只有 1-n 个点,说明有环,而这个环还是更新过的,说明有负权环)

代码易错点:
dist[b] = min(dist[b], backup[a] + w); ,更新 dist[b] 的时候是在 dist[b]backup[a] + w 之间去更新的,而不是 backup[b]backup[a] + w
if(ans > 0x3f3f3f3f / 2) cout << "impossible";,也许最后一个点的最短值被更新后,依然是争取穷,但已经不等于 0x3f3f3f3f 了,比它略小一点

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
struct g
{
    int a, b, c;
}edgs[N];
int n, m, k;
int dist[N], backup[N];
int Bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for(int i = 0; i < k; i++) //循环 k 次
    {
        memcpy(backup, dist, sizeof dist);
        for(int j = 0; j < m; j++) // m 条边 
        {
            int a = edgs[j].a, b = edgs[j].b, w = edgs[j].c;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    return dist[n];
}
int main()
{
    cin >> n >> m >> k;
    for(int i = 0; i < m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        edgs[i] = {a, b, c};
    }
    int ans = Bellman_ford();
    if(ans > 0x3f3f3f3f / 2) cout << "impossible";
    else cout << ans;
    return 0;
}

spfa算法 求最短路

spfa 算法是对 Bellman-ford 算法做了优化
时间复杂度:一般为 O(m),最坏为 O(nm)
在遍历每条边后,后续并不是所有边都需要遍历,只有前面更新过的节点,才能作为最短路来更新其他节点,因此每次只需要将更新过的节点加入到队列,每次取队头节点作为最短路来更新节点(更新邻接表中它作为队头所能到达的所有节点,如果更新成功并且那个节点不在队列中,就放入队列)
当队列为空时,就更新完了所有节点!

代码易错点:st 数组标志的是某个节点是否在队列中,当将一个节点加入到队列和 pop 出队列后,都要及时修改 st 数组

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int e[N], ne[N], w[N], h[N], idx;
int dist[N];
int st[N]; // 记录节点是否在队列中
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
    idx ++;
}
int spfa()
{
    memset(dist, 0x3f3f3f3f, sizeof dist); // 初始化 dist 数组, 保证每次更新正确
    dist[1] = 0; 
    queue<int> q;
    q.push(1);
    st[1] = true;
    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false; // 节点出队列后, 就要马上改为 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]) // 只有不在队列中时, 才将其加入队列 
                {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    return dist[n];
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    int ans = spfa();
    if(ans > 0x3f3f3f3f / 2) cout << "impossible";
    else cout << ans;
    return 0;
}

spfa 算法判断负权回路

可以适用 spfa 算法判断有向图中是否有负权回路

但相比于 spfa 在求带负权边最短路,它在求是否存在负权回路时,需要将所有点都加入到队列中,因为某些点可能带有负权回路,但它不能到终点或者和最短路无关。

改动方法:只需要多维护一个 cnt 数组,先初始化为 0,每当 dist[j]被更新一次,就把对应的 cnt[j] ++当cnt 数组里的某个值大于等于 n 的时候,就说明出现了负权回路(抽屉原理)

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int e[N], ne[N], w[N], h[N], idx;
int dist[N], cnt[N]; // dist 数组记录距离, cnt 数组记录边数 
int st[N]; // 记录节点是否在队列中
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
    idx ++;
}
bool spfa()
{
    // memset(dist, 0x3f3f3f3f, sizeof dist);
    // dist[1] = 0;  // 没有必要初始化了, 因为有负权边, dist[j] > dist[t] + w[i] 可以成立 
    queue<int> q;
    for(int i = 1; i <= n; i++) 
    {
        q.push(i);
        st[i] = true;
    }
    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false; // 节点出队列后, 就要马上改为 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]) // 只有不在队列中时, 才将其加入队列 
                {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    return false;
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    bool ans = spfa();
    if(ans) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

Floyd 算法求多源最短路

多源最短路(含负权),可以使用 Floyd 算法求某任意两个点之间的最短路

借助动态规划的思想,进行三层循环,在循环中持续更新 dist 数组,在三层循环结束后,dist[x][y] 中存的就是 x -> y 的最短路径

三层循环,k 在最外,i 次之,j 在最里面 ( 都是循环 n 次 )
宏观上记忆,i 到 j 的最短路,就等于 i 先到 k ,再从 k 到 j 的最短路之和

直接利用邻接矩阵,三层循环!

for(int k = 1; k <= n; k++) // 动态规划
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= n; j++)
		{
			if(i == j) d[i][j] = 0; // 如果有自环, 那么询问 i->i 时的距离就是 0 
			d[i][j] = min(d[i][j], d[i][k] + d[k][j]); 
			// 宏观上记忆, i->j == i->k + k->j
		}

代码实现:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int n, m, k;
int d[N][N]; 
void floyd()
{
    for(int k = 1; k <= n; k++) // 动态规划
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
            {
                if(i == j) d[i][j] = 0; // 如果有自环, 那么询问 i->i 时的距离就是 0 
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // 宏观上记忆, i->j == i->k + k->j
            }
}
int main()
{
    cin >> n >> m >> k;
    memset(d, INF, sizeof d);
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        d[a][b] = min(d[a][b], c); // 保留最小的长度
    }
    floyd();
    while(k--)
    {
        int x, y;
        cin >> x >> y;
        if(d[x][y] > INF / 2) cout << "impossible" << endl;
        else cout << d[x][y] << endl;
    }
    return 0;
}

宽搜解决边权为1的最短路问题

只需要将起点加入队列,然后进行一次宽搜,即可将所有距起点的最短路记录在 d 数组里
如果起点有多个(多源最短路问题),那么只需要将所有起点初始加入到队列中即可!

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int m, n;
int e[N], ne[N], d[N], idx, h[N];
void add(int a, int b) // 建立从 a --> b 的有向边
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
int bfs()
{
    queue<int> q;
    memset(d, -1, sizeof d); // 初始化距离数组全部为 -1, 并且如果等于 -1 表示未被遍历过
    d[1] = 0; // 初始位置距离初始化为 0
    q.push(1); // 宽搜模板 
    while(q.size() > 0)
    {
        int t = q.front();
        q.pop();
        for(int i = h[t]; i != -1; i = ne[i]) // 遍历图的方法
        {
            int j = e[i];
            if(d[j] == -1)
            {
                q.push(j);
                d[j] = d[t] + 1;
            }
        }
    }
    return d[n];
}
int main()
{
    memset(h, -1, sizeof d); // *** 初始化所有的头节点!***
    cin >> n >> m;
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    cout << bfs() << endl;
} 
  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值