[补充]:最短路复习

随着2020的即将到来,似乎越来越忙了(hh,当然不是忙于准备年货,而是忙于补各种作业).
今天下午难得有段闲暇时光,复习一下(之前就没理解透透的吧) 图论中的最短路部分,以
便更好的学习.
最短路算法:
1、Dijkstra(一般): 通过每次循环找全局最小值进行更新其他节点(时间复杂度:O(n^2))
   Dijkstra(优化):通过二叉堆(优先级队列)找最小值(队首如果就是最小值的话,我们就不用每次都去寻找最小值了)
                     (时间复杂度:O(mlog(n))
2、Bellman-Ford    :(后续补充,先学了SPFA,hhhhh)
3、SPFA(Bellman-ford的优化) : 通过队列(类似于BFS,每个点可能出队、入队多次)来实现每个顶点的更新,直到队列
                     为空为止.
                     (时间复杂度(km),其中 k 是一个较小的常数)
4、Floyd(任意两点间的最短路):
                      (时间复杂度:O(n^3)

下面逐个介绍一下各个算法及实现代码:

1、Dijkstra: 
  优点:优化后的算法时间复杂度相对于其他算法来说较低
  缺点:无法处理有负边权的图(这是重点),也就是目光短浅,具体怎么短浅稍后细说.
  算法流程:
  1、初始化dist[1] = 0,(因为本身到自己的距离是 0 ),其余节点的设置为正无穷大(memset(dist,0x3f,sizeof(dist));
  2、找出一个**未被标记的**、dist[x]最小的节点 x ,然后标记节点 x.
  3、扫描节点 x 的所有出边(x,y,z),也就是所有跟节点 x 相连的边,若dist[y] > dist[x] + z,则使用dist[y] = dist[x] + z进行更新,dist[y].(这里用到了三角不等式的原理).
  4、重复上述2 ~ 3 个步骤,直到所有节点都被更新.
  用图来理解更清晰:

具体看这位大佬的图解: https://www.acwing.com/blog/content/462/ (十分详细)

  (懒得画了,hhhhhhh)
  为啥不能处理负权?
  首先我们要清楚一个点:Dijkstra是每次贪心的选择跟当前邻接的点,而不会去考虑处邻接之外的其他点,举个例子来说:
  
    8
  A ---- B
  |    /
  |   /
10|  / -4
  | /
  C
从 A -- > B 我们可以很明显看出最短路径是 A - > C - > B (10 + (-4)) = 6;
  但是通过Dijkstra算法我们得到的结果是 A - > B (8),答案是 8;
  (可以自己试一下)
  因为我们从起点 A 开始,最先开始找的是 A -> B 所花费的路径短还是 A -> C 花费的路径短,当到 B (第一次选择的结果,就会从B开始在找与B相关的最短的边)(所以如果我们用Dijkstra来处理有负权值得图时得到得答案很可能是不正确的).

代码部分(一般): 题目链接: https://www.acwing.com/problem/content/851/
(需要修改一下输出才能AC哦)

    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<string.h>
    #include<cstdio>
    #include<string>
    #define INF 0x3f3f3f3f                           // 表示最大值
    using namespace std;
    const int maxn =505;
    int a[maxn][maxn],vis[maxn],dist[maxn];          // 采用邻接矩阵来存图(消耗得内存较大)
    int n,m;
    int main(void) {
        void Dijkstra();
        memset(a,0x3f,sizeof(a));
        memset(dist,0x3f,sizeof(dist));              // 一定要记得初始化为正无穷大 
        memset(vis,0,sizeof(vis));                   // 标记
        scanf("%d%d",&n,&m);
        int x,y,w;
        for(int i = 1; i <= m; i ++) {
            scanf("%d%d%d",&x,&y,&w);
            a[x][y] = min(a[x][y],w);                // 可能会有重边(重复的边),所以需要取最小值
        }
        Dijkstra();
        for(int i = 1; i <= n; i ++ ){
            printf("%d\n",dist[i]);
        }
        return 0;
    }
    void Dijkstra() {
        int x = 0;                           // 用来记录每次全局最小值得下标
        dist[1] = 0;                         // 刚开始与自身的距离是 0 
        for(int i = 1; i < n; i ++) {        // 循环 N - 1 次(因为起点已经更新了,还剩下 N - 1 个点未更新)
            x = 0;
            for(int j = i; j <= n; j ++) {
                if(vis[j] == 0 && (x == 0 || dist[j] < dist[x])) { // 选取未标记过的全局最小值
                    x = j;
                }
            } 
            vis[x] = 1;                      // 标记已选中的顶点
            for(int j = 1; j <= n; j ++) {   // 更新与该顶点相关的边
                dist[j] = min(dist[j],dist[x] + a[x][j]);
            }
        }
        return ;
    }
      
Dijkstra(优化) :(采用邻接表的存储方式来存图(模拟数组链表的方式))

题目链接: https://www.acwing.com/problem/content/852/
(需要修改一下输出才能AC哦)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<string>
#include<queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e5 + 6;
priority_queue<pair<int,int> >pq;                        //优先级队列默认是从大到小进行排序的(我们每次插入队列中这个数的相反数,自然就能表示这个数的最小值)
int vis[maxn],head[maxn],Next[maxn],edge[maxn],ver[maxn];
int dist[maxn];
int n,m,tot;
int main(void) {
    void Dijkstra();
    void add(int x,int y,int w);
    int x,y,w;
    memset(vis,0,sizeof(vis));
    memset(dist,0x3f,sizeof(dist));
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= m; i ++) {
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w);
    }
    Dijkstra();
    for(int i = 1; i <= n; i ++ ){
        printf("%d\n",dist[i]);
    }
    return 0;
}
void add(int x,int y,int w) {
    ver[++tot] = y,edge[tot] = w;        // ver[]:表示每条边的终点,edge[]:表示每条边的权值
    Next[tot] = head[x],head[x] = tot;   /* Next[]:存储上一条边的序号,用来进行连接(遍历)
    插入到表头                              表示从相同节点出发的下一条边在ver和edge数组中的存储位置 
                                          */
    return ;                             // head[]:记录从每个节点出发的第一条边在ver 和 edge 数组中的存储位置
}
void Dijkstra() {
    dist[1] = 0;
    pq.push(make_pair(0,1));             // first:权值大小,second:该权值对应的节点
    while(pq.size()) {
        int x = pq.top().second;pq.pop();
        if(vis[x]) continue;
        vis[x] = 1;
        // 遍历与该节点相关的所有边(更新)
        for(int i = head[x]; i ; i = Next[i]) {
            int y = ver[i],z = edge[i];
            if(dist[y] > dist[x] + z) {
                // 将更新后的最小值(值和对应的节点)组合重新插入到队列中,以便于后面寻找更小的(每次都要是最小距离)
                dist[y] = dist[x] + z;
                pq.push(make_pair(-dist[y],y)); 
            }
        }
    }
    return ;
}
2、SPFA()算法:
   优点:SPFA()算法最大的优点莫过于可以处理负边权,而且优化过后的时间复杂度与Dijkstra()算法相差不大
   缺点:仅用队列实现的SPFA算法时间复杂度还是有点高的,因为它每次顶点可能会多次进行出队、入队、这也就是它可以      处理负边权的最大原因。
   算法流程:
   1、建立一个队列,最初队列只包含起点 1
   2、取出队头节点x,扫描它的所有出边(x,y,z),若dist[y] > dist[x] + z,则使用dist[x] + z更新dist[y].同时,若
   y 不在队列中,则把 y 入队,并且进行标记.
   3、重复上述步骤,直到队列为空。
   该算法的队列都保存了待扩展的节点(这点也很重要,可以拿上面的例子模拟一下).每次入队都相当于完成一次dist数组的更新操作,使其满足三角形不等式.
   一个节点可能会出队、入队多次。最终,图中节点收敛到全部满足三角形不等式的状态。
   代码部分:

题目链接: https://www.acwing.com/problem/content/853/
(需要修改一下输出才能AC哦)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<string.h>
#include<string>
#include<queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e5 + 5;
int head[maxn],Next[maxn],edge[maxn],ver[maxn];
int dist[maxn],vis[maxn];
int n,m,tot;
int main(void) {
    void add(int x,int y,int w);
    void spfa();
    memset(dist,0x3f,sizeof(dist));
    memset(vis,0,sizeof(vis));
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= m; i ++) {
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w);
    }
    spfa();
    for(int i = 1; i <= n; i ++) {
        cout<<dist[i]<<endl;
    }
    return 0;
}
void add(int x,int y,int w) {
    ver[++tot] = y,edge[tot] = w;
    Next[tot] = head[x],head[x] = tot;
    return ;
}
void spfa() {
    queue<int>q;
    dist[1] = 0,q.push(1);
    vis[1] = 1;
    while(!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0;                              // 出队后就标记为未使用过
        for(int i = head[x]; i ; i = Next[i]) {
            int y = ver[i],z = edge[i];
            if(dist[y] > dist[x] + z) {
                dist[y] = dist[x] + z;           // 这里只要符合三角式定理,就更新
                if(vis[y] == 0) {
                    q.push(y);                   // 每次入队的是未标记过的顶点
                    vis[y] = 1;                  // 入队后就标记已使用了
                }
            }
        }
    }
    return ;
}
posted @ 2020-04-28 21:17  IceSwords  阅读(115)  评论(0编辑  收藏  举报