最短路算法



最短路总览

最短路


朴素Dijkstra - 稠密图 - O ( n 2 ) O(n^2) O(n2)

具体思路

  • 我们需要三个数组,分别是记录边的g[][]、记录到 1 1 1 节点距离的dist[]、还有标记节点是否已经求得最短路的st[]
  • 将距离数组初始化为 I N F INF INF ,然后添加第一个最短距离:dist[1] = 0,意思就是 1 1 1 节点到 1 1 1 节点的最短距离是 0 0 0
  • 寻找没有确定最短距离的节点中距离最短的那个点
    • 将这个点的距离标记为已经求得最短距离了(因为是所有未确定中最小的)
    • 用这个点的所有边去更新其连接的其他节点
  • 重复上一步,一轮能确定一个点的最短路,重复 n n n 轮就能将所有点的最短路找到
  • 当然也可以在中间加个优化:如果第 n n n 个点的最短路已经确定,那就直接退出返回即可

时间复杂度分析

重复 n n n 轮,每轮遍历所有节点找最小值,然后遍历所有边,所以是 O ( n ⋅ ( n + n ) ) O(n · (n + n)) O(n(n+n)),也就是 O ( n 2 ) O(n^2) O(n2)


使用场景

  • 稠密图尽量使用朴素 D i j k s t r a Dijkstra Dijkstra
    • 稠密图: m ≈ n 2 m ≈ n^2 mn2
    • 稠密图用邻接矩阵
  • 因为时间复杂度: O ( n 2 ) < O ( n 2 ⋅ l o g n ) O(n^2) < O(n^2·logn) O(n2)<O(n2logn) ,朴素比堆优化快

AcWing 849. Dijkstra求最短路 I

题目链接:https://www.acwing.com/activity/content/problem/content/918/

Dijkstra1

CODE
#include <iostream>     // 用于输入和输出
#include <cstring>      // 用于处理字符串
#include <algorithm>    // 用于算法操作

using namespace std;

const int N = 520;      // 定义常量N
int n, m;               // 定义整型变量n和m
int g[N][N];            // 定义二维数组g
int dist[N];            // 定义一维数组dist
bool st[N];             // 定义布尔数组st

int dijkstra(){         // 定义dijkstra函数
    memset(dist, 0x3f, sizeof dist);  // 初始化dist数组
    dist[1] = 0;        // 设置dist数组的第一个元素为0

    for(int i = 1; i <= n; ++i){  // 遍历n个节点
        int t = -1;
        for(int j = 1; j <= n; ++j)  // 寻找未被访问的距离最小的节点
            if(!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;

        st[t] = true;   // 标记节点t已被访问

        for(int j = 1; j <= n; ++j)  // 更新所有节点到源点的距离
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }

    // 如果dist[n]的值未改变,返回-1,否则返回dist[n]
    return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}

int main()  // 主函数
{
    cin >> n >> m;  // 输入n和m
    memset(g, 0x3f, sizeof g);  // 初始化g数组

    int a, b, c;
    while (m -- ){  // 输入m条边
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);  // 更新边的权值
    }

    int t = dijkstra();  // 调用dijkstra函数

    cout << t << endl;  // 输出结果
}



堆优化 D i j k s t r a Dijkstra Dijkstra 算法 - 稀疏图 - O ( m l o g n ) O(mlogn) O(mlogn)

我们可以发现,朴素 D j i k s t r a Djikstra Djikstra 一共分为三步:

  • 遍历所有节点
    • 找出最小路径的节点
    • 由这个节点更新其他节点的距离

其中最后两步:找出最小和更新距离可以用我们学过的一个数据结构 - 来优化


具体思路和时间复杂度分析

  • 其他地方不变,将更新过的节点用堆来储存,这样就可以将查询时间从 O ( n ) O(n) O(n) 降到 O ( 1 ) O(1) O(1),也就是说不用再遍历数组了
  • 每次弹出一个元素对其他值进行修改,复杂度就是 O ( m ⋅ l o g n ) O(m · logn) O(mlogn)
    • 这个地方其实分手写堆还是 S T L STL STL
      • 手写堆:我们自己能保证堆中只有 n n n 个元素,弹出后修改某一元素的值。那么弹出元素全部修改一遍需要 O ( l o g n ) O(logn) O(logn) ,而排空堆一共需要 m m m 次,所以一共是 O ( m ⋅ l o g n ) O(m·logn) O(mlogn)
      • S T L STL STL:我们每次更新了一个节点的值就将这个值入堆,所以说堆中一共会有 m m m 个元素,那么等全部循环完就是 O ( m ⋅ l o g m ) O(m·logm) O(mlogm)
    • 在最坏情况的稠密图中, S T L STL STL 堆复杂度是 O ( m ⋅ l o g m ) O(m·logm) O(mlogm) ,也就是 O ( n 2 ⋅ l o g ( n 2 ) ) O(n^2·log(n^2)) O(n2log(n2)) ,化简得 O ( 2 n 2 ⋅ l o g n ) O(2n^2·logn) O(2n2logn) ,这个复杂度跟手写堆的 O ( n 2 ⋅ l o g n ) O(n^2·logn) O(n2logn) 差不多,所以我们用优先队列
  • 结论就是,复杂度为: O ( m ⋅ l o g n ) O(m·logn) O(mlogn)

使用场景

  • 适用于稀疏图
    • 稀疏图: m ≈ n m ≈ n mn
    • 稀疏图用邻接表
  • 因为时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn) 比朴素版 O ( n 2 ) O(n^2) O(n2) 低:

AcWing 850. Dijkstra求最短路 II

题目链接:https://www.acwing.com/activity/content/problem/content/919/

Dijkstra2

CODE
#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], ne[N], w[N], idx;
int dist[N];
bool st[N];
int n, m;
int a, b, c;

void add(int a, int b, int c){
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    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});
    
    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] > dist[ver] + w[i]){
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m -- ){
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    cout << dijkstra() << endl;
}


D i j k s t r a Dijkstra Dijkstra 算法的局限

  • D i j k s t r a Dijkstra Dijkstra 算法没办法处理负权图,为什么呢?
    1
    Dijkstra缺陷

  • 总结来说,就是 D i j k s t r a Dijkstra Dijkstra 算法采用了贪心思想,所以出错,原因如下:

    • 每次取最小的值为基准,更新其他值,然后这个点被打上标记,不能再用来更新其他值,但是它的值可以被更新。
    • 如果说存在负权边,那么我之前打上标记的节点的值可能就不是最短路的值了,虽然这个点的dist[]值可以被更新,但是要想再用正确的最短路的值更新其他节点就做不到了(因为被打上标记了),所以最后的答案是可能是错的。
  • 它们一刻也没有为 D i j k s t r a Dijkstra Dijkstra 算法而哀悼,立刻赶来战场的是 — B e l l m a n − f o r d Bellman-ford Bellmanford 算法 S P F A SPFA SPFA 算法!!!



Bellman - Ford 算法 - 随便存边 - O ( n m ) O(nm) O(nm)

具体思路

bellmanford

  • 首先,循环 k k k 次(题目指定最大次数,没指定就是节点数):
    • 每次循环遍历所有边,更新可以更新的边,俗称松弛操作
  • 其次,没有了,就这么多,是的,就这么多。
  • 所有操作做完之后可以保证公式: d i s t [ b ] ≤ d i s t [ a ] + w dist[b] ≤ dist[a] + w dist[b]dist[a]+w俗称三角不等式
    • 为什么强调 B e l l m a n − f o r d Bellman-ford Bellmanford 满足这个公式呢?
      因为 D i j k s t r a Dijkstra Dijkstra 并不满足(有负权边时),之前说过 D i j k s t r a Dijkstra Dijkstra 的缺陷,这就导致了如果存在负权边时,有些节点的值并不是真正的最短路,也就不满足以上公式了。
  • 由于满足这个公式,所以以上操作做完,求得的所有点的值都是 k k k 次操作内的最短路了。

时间复杂度分析

两遍循环,一重循环循环 n n n 次,每次循环都遍历每条边,把能更新的都更新一遍,那么就是 O ( n m ) O(nm) O(nm)


使用场景

  • 有负权的图
    • 但是不能有负环,有负环的话就会在负环上无限绕,得到的最短路就是负无穷-INF
  • 对操作数有限制的题
    • 有些题目会对走的次数限制在 k k k 次,那么我们就在外围循环循环 k k k 次而不是 n n n 次。
      也就是最多不经过 k k k 条边的意思。
    • 如果没有限制,那就循环 n n n 次,把所有点都轮一遍。但是一般情况下都会用之后说的 S P F A SPFA SPFA 算法。
  • 带负环的图其实也可以做,当我们进行完所有松弛操作后,所有节点的值都满足三角不等式,这个时候我们再进行一次松弛操作,如果检测出来还有能操作的点,那么一定存在负环。
    • y总说的是:走了 n n n 次,那么就一定有 n + 1 n + 1 n+1 个节点,而我们一共也就 n n n 个点,由抽屉原理得一定存在负环。
      这里应该说的是证明,但是判负环基本不用 B e l l m a n − f o r d Bellman-ford Bellmanford 来做,而是用 S P F A SPFA SPFA 算法,因为。
    • 本题的边可以用任意方式存,只要能被遍历到即可,所以直接拿结构体数组存了。

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

题目链接:https://www.acwing.com/activity/content/problem/content/922/

bellmanford题目

CODE
#include <iostream> 	// 引入输入输出流库
#include <cstring> 		// 引入字符串处理库
#include <algorithm> 	// 引入算法库
#define INF 0x3f3f3f3f 	// 定义无穷大的值

using namespace std;

const int N = 510, M = 10010; 	// 定义常量N和M

struct{ 	// 定义一个结构体,用于存储边的信息
    int a, b, c;
}edges[M];

int n, m, k; 	// 定义变量n,m,k
int a, b, c; 	// 定义变量a,b,c
int dist[N], backup[N]; 	// 定义数组dist和backup

void bellman_ford(){ 	// 定义Bellman-Ford算法函数
    memset(dist, 0x3f, sizeof dist);	 // 初始化dist数组
    dist[1] = 0; 		// 将dist数组的第一个元素设为0
    
    for(int i = 0; i < k; ++i){ 			// 对每个顶点进行遍历
        memcpy(backup, dist, sizeof dist); 	// 将dist数组的值复制到backup数组
        for(int j = 0; j < m; ++j){ 		// 对每条边进行遍历
            auto e = edges[j]; 				// 获取当前边的信息
            dist[e.b] = min(dist[e.b], backup[e.a] == INF ? INF : backup[e.a] + e.c); // 更新dist数组的值
        }
    }
}

int main() // 主函数
{
    cin >> n >> m >> k; 				// 输入n,m,k的值
    for(int i = 0; i < m; ++i){ 		// 对每条边进行遍历
        scanf("%d%d%d", &a, &b, &c); 	// 输入a,b,c的值
        edges[i] = {a, b, c}; 			// 将a,b,c的值存储到edges数组
    }
    
    bellman_ford(); // 调用Bellman-Ford算法函数
    
    if(dist[n] == INF) puts("impossible"); // 如果dist数组的最后一个元素等于无穷大,输出"impossible"
    else printf("%d\n", dist[n]); // 否则,输出dist数组的最后一个元素的值
}

代码疑问点解析
  • backup[]数组:
    • 备份数组,用于备份上一次更新时所有最短路的值。
    • 为什么要备份?
      • 在一次外重循环中,会对所有的边进行遍历,把能更新的更新了,但是这样就会有个问题,我先更新的值可能会对后面的更新产生影响。
        比如有一条路 A − B − C A - B - C ABC , A A A 有最短路,而 B , C B, C B,C I N F INF INF ,没有最短路,题目给定只许进行一次操作。很显然,只走一步的话 C C C 是不可能有最短路的,但是错误发生了:先遍历 A − B A - B AB 边, B B B 获得最短路,那么到这应该结束了,但是循环内会把所有边都遍历一遍,如果没有备份数组,那么 C C C 就会由 B B B 而获得最短路。
        我们一次外层循环想得到的结果是根据已经有最短路的节点去更新没有最短路的节点,而且仅仅只走一步,当没有备份时,我们可能会向外发散很多步
  •   dist[e.b] = min(dist[e.b], backup[e.a] == INF ? INF : backup[e.a] + e.c);
    
    • 这步操作的意思是:
      • 如果有一条边 A − B A - B AB ,边权为负数,比方说为 − s -s s,且两个都没最短路,那么更新的时候只用min()函数比较更新前后的值的时候就会把 B B B 的最短路更新为 I N F − s INF - s INFs ,但是其实 B B B 依然没有最短路,对于终点情况也是一样。
      • 如果只写一个裸的min()函数,不进行判断的话,需要在最后判断是否有最短路的时候把判断改为
        if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
        
        只要大于一个不可能的数就判断为没有最短路。


S P F A SPFA SPFA 算法 - 一般 O ( n ) O(n) O(n),最坏 O ( n m ) O(nm) O(nm)

有两个应用

  • 求最短路
  • 判负环

具体思路 - 求最短路

B e l l m a n − f o r d Bellman-ford Bellmanford 算法优化一下,每次不将所有边都遍历一遍,而是将更新后的节点入队,每次从队列里面拿节点进行更新。


时间复杂度分析

  • 从第一个节点开始入队,最好的情况就是每个节点只需要更新一次,那么就是 O ( n ) O(n) O(n)
  • 最坏的情况是:
    spfa

AcWing 851. spfa求最短路

题目链接:https://www.acwing.com/activity/content/problem/content/920/

在这里插入图片描述

CODE
#include <iostream> 	// 引入输入输出流库
#include <cstring> 		// 引入字符串处理库
#include <algorithm> 	// 引入算法库
#include <queue> 		// 引入队列库
#define INF 0x3f3f3f3f 	// 定义无穷大的值

using namespace std;

const int N = 1e5 + 10; 	// 定义常量N
int h[N], e[N], ne[N], w[N], idx; 	// 定义数组h,e,ne,w和变量idx
int n, m; 		// 定义变量n,m
int a, b, c; 	// 定义变量a,b,c
bool st[N]; 	// 定义布尔数组st
int dist[N]; 	// 定义数组dist

void add(int a, int b, int c){ 	// 定义添加边的函数
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

int spfa(){ 	// 定义SPFA算法函数
    memset(dist, 0x3f, sizeof dist); 	// 初始化dist数组
    dist[1] = 0; 	// 将dist数组的第一个元素设为0
    
    queue<int> q; 	// 定义一个队列q
    q.push(1); 		// 将1压入队列
    st[1] = true; 	// 将st数组的第一个元素设为true
    
    while(q.size()){ 	// 当队列不为空时
        auto t = q.front(); 	// 获取队列的第一个元素
        q.pop(); 		// 将队列的第一个元素弹出
        st[t] = false; 	// 将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个元素加上边的权值
                dist[j] = dist[t] + w[i]; 	// 更新dist数组的第j个元素的值
                if(!st[j]){ 	// 如果st数组的第j个元素为false
                    q.push(j); 	// 将j压入队列
                    st[j] = true; // 将st数组的第j个元素设为true
                }
            }
        }
    }
    
    return dist[n]; 	// 返回dist数组的最后一个元素的值
}

int main() // 主函数
{
    memset(h, -1, sizeof h); 	// 初始化h数组
    
    cin >> n >> m; 	// 输入n,m的值
    
    while (m -- ){ 		// 对每条边进行遍历
        scanf("%d%d%d", &a, &b, &c); 	// 输入a,b,c的值
        add(a, b, c); 	// 调用add函数,添加边
    }
    
    int ans = spfa(); 	// 调用spfa函数,获取最短路径的长度
    
    if(ans == INF) puts("impossible"); 	// 如果ans等于无穷大,输出"impossible"
    else printf("%d\n", ans); 			// 否则,输出ans的值
}

代码细节
  • st[]数组:用来标记队列中的节点,已经入队了就没必要再次入队了,防止无限入队。
  • if判断是否更新而不是直接用min()函数:防止无限入队。

具体思路 - 判负环

维护一个cnt[]数组,每当某节点被更新一次就在源点对应的记录数组cnt[]+1,当某个记录数突破了节点数时,肯定存在负环,证明详情见 B e l l m a n − f o r d Bellman-ford Bellmanford 算法y总原话之抽屉原理


AcWing 852. spfa判断负环

题目链接:https://www.acwing.com/activity/content/problem/content/921/

spfa判负环

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

using namespace std;

const int N = 2010, M = 10010;
int n, m;
int a, b, c;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N];
bool st[N];

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

bool spfa(){
    //memset(dist, 0x3f, sizeof dist);	加不加都行
    //dist[1] = 0;
    
    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;
        
        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(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
                
                if(cnt[j] >= n) return true;
            }
        }
    }
    
    return false;
}

int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    while (m -- ){
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    if(spfa()) puts("Yes");
    else puts("No");
}
代码疑点解读
  • 为什么一开始把所有点入队:
  • 为什么dist[]不用初始化:
    • 因为如果边权为正数,dist[]永远不会更新,直到遇见了负数才会更新,如果有负环那就会一直更新直到爆 n n n 返回false


F l o y d Floyd Floyd 算法 - O ( n 3 ) O(n^3) O(n3)

思路简介

采用了动态规划思想,具体怎样我也不知道 >_<,没学过呜呜呜。
总之很短很暴力。
别问,问就是背过

菜就多练,学不会就背过。
暴力是暴力, F l o y d Floyd Floyd F l o y d Floyd Floyd
你要是一直拿 F l o y d Floyd Floyd 当暴力。
你咋不去自己写一个?


时间复杂度分析

三重循环所以是—— O ( n 3 ) O(n^3) O(n3)
太美丽了家人们。


AcWing 854. Floyd求最短路

题目链接:https://www.acwing.com/activity/content/problem/content/923/

floyd

#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

int n, m, k;
const int N = 210, M = 20010;
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)
                d[i][j] = min(d[i][j], 
                	(d[i][k] == INF || d[k][j] == INF) ? INF : d[i][k] + d[k][j]);
               	// y总写的d[i][j] = min(d[i][j], d[i][k] + d[k][j]);              
}

int main()
{
    cin >> n >> m >> k;
    
    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;
            
    while (m -- ){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);
    }
    
    floyd();
    
    while(k--){
        int a, b;
        scanf("%d%d", &a, &b);
        
        if(d[a][b] == INF) puts("impossible");
        else printf("%d\n", d[a][b]);
    }
}
代码疑点解读
  • 为什么跟y总不一样的地方能过,什么思路?
    • 这个跟 B e l l m a n − f o r d Bellman-ford Bellmanford 一样,把所有边轮了一遍,所以会出现之前说的INF - k的情况出现,所以加了个判断。
    • 至于为什么这么写,我只能说我是仿照 B e l l m a n − f o r d Bellman-ford Bellmanford 写的判断,但是只判断前一个或后一个都会 WA,但是一起判断了就AC了,所以我就这么写了,原理我也不懂,嘿嘿。
  • 为什么初始化的时候把每个自己到自己的节点距离设为 0 0 0
    源点初始化

写了一天总算写完了,真的很复杂啊 %%%%%%%%%%%%%%

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值