算法基础3.2最短路算法:Dijkstra(朴素和堆优化迪杰斯拉),bellman-ford(贝尔曼ford算法),spfa(优化的贝尔曼),Floyd(多源汇)

常见的最短路问题,一般来说可以分成两大类,第一大类是单源最短路问题,第二大类就是多源汇最短路。单源最短路,一般来说是求从一个点到其他所有点的最短距离,比方说最常见的一种题型是从一号点到n号点的一个最短路径,就是求出来从从一号点到其他所有点的最短路之后,那么从一号点到n号点的最短路也就求出来了,多源汇最短路,源点就是起点,一般来说在图论里边,源点就是起点。汇点一般来说就是终点。多源汇的最短路问题就是说可能不止一个起点,可能会有很多个询问,每个询问是任选两个点,然后求从其中一个点走到另外一个点的最短距离,起点和终点都是不确定的,这两大类里面可以再细分,首先单源最短路里面,可以再细分成两大类。第一类是所有边都是正权值的图,第二类就是存在负权边,就是说图里边某些的权重可能是负数,首先,所有边的权重都是正数,图论里边有个很经典的算法,这个算法的不同的实现方式是适用于不同场景的。首先第一个是朴素的迪杰斯拉算法,第二个是堆优化版的迪杰斯拉算法,约定n表示图里面点的数量,m表示边的数量。朴素版的狄耶斯拉算法的时间复杂度就是n平方的,堆优化版的迪杰斯拉算法的时间复杂度就是 logn的。稠密图的一般来说是指边数m和n方是一个级别的,如果m和n是一个级别的叫稀疏图,这种数据范围一般在算法题里面很明显。比方说n和m都是十万,显然就是一个稀疏图,如果n是100,m是一万,显然就是一个稠密图。可以发现朴素版的狄杰斯拉算法的时间复杂度和边数是没有关系的,所以说朴素版的狄耶斯拉算法比较适合于稠密图,用邻接矩阵来存,就是当边数比较多的时候,比方说边数和on方是一个级别的时候,那么应该用朴素版的迪杰斯拉算法,它的时间复杂度是平方的。如果说稠密图用堆优化版的迪杰斯拉算法,那么它的时间复杂度就是m log n的,m和n方是差不多一个级别的。那么它的一个时间复杂度就会变成n方log n,它的时间复杂度比朴素版的要稍微高一些。因此如果是稠密图,那么就要尽量使用朴素的迪杰斯拉算法,那反之的话,如果说是一个稀疏图,比方说m和n是一个级别,都是十万的级别。此时就不能用朴素版的迪杰斯拉了,因为n平方就是一个十的十次方,就会超过时间限制。但是可以用堆优化版的迪杰斯拉,用邻接表来存,它的时间复杂度只需要m log n,大概也就十万log十万,就可以很快的解决。该算法仍只需要考虑有向图,因为无向图是一种特殊的有向图,连两条路就行了。

另外一种图就是图里面存在一些负权边,也有两种实现方式,首先第一种就是贝尔曼ford算法,它的时间复杂度是on*m,就是点数乘边数。还有另外一个算法叫spfa,它是对贝尔曼ford算法进行了优化,它的时间复杂度基本上可以看成om。平均的时间复杂度是线性的,如果出题人比较变态的话,时间复杂度是Onm的,但是它的效率一般来说都会比贝尔曼ford的算法要高的,虽然spfa算法是贝尔曼ford算法的一个优化,但是并不是所有情况下用spfa算法都可以做,比方说举一个例子,就是如果说想求一下经过不超过k条边的最短路,就是对最短路的经过的边数做一个限制,如果要是限制经过的边数小于等于k,那么此时就只能用贝尔曼ford来做。

最后一种情况是多源汇最短路,只有一个是经典的弗洛伊德算法。然后它的时间复杂度是on的 3次方。

最短路算法在未来遇到的问题中有一个很大的特点,考察的侧重点是建图,就是如何把原问题抽象成一个最短路的问题。如何来定义点和边?使得这个问题变成一个最短路的问题,然后套用模板来做,狄杰斯拉算法基于贪心,弗洛伊德基于动态规划,贝尔曼ford算法基于离散数学的一些知识,

朴素版迪杰斯拉算法

首先迪杰斯拉算法是单源最短路,就是它求的是从一号点到其他所有点的一个最短距离。

算法步骤是这样的,首先第一步先初始化距离让dist1等于零,就是一号点到起点的距离是零,然后其他所有的点都等于正无穷。就是第一步的时候,只有起点被遍历到了,只有起点的距离是确定的,其余所有点距离都是没有确定的。

例题:Dijkstra求最短路 I

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式

第一行包含整数 n 和 m。

接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n≤500,
1≤m≤10的5次方,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

朴素dijkstra算法 
时间复杂是 O(n2+m), n表示点数,m表示边数
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];
}
堆优化版dijkstra —— 模板题 AcWing

作者:yxc
链接:https://www.acwing.com/blog/content/405/
来源:AcWing

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

const int N = 510, M = 100010;

int h[N], e[M], ne[M], w[M], idx;//邻接表存储图
int state[N];//state 记录是否找到了源点到该节点的最短距离
int dist[N];//dist 数组保存源点到其余各个节点的距离
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 Dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));//初始化dist 数组的各个元素为无穷大
    dist[1] = 0;//源点到源点的距离为置为 0
    for (int i = 0; i < n; i++)
    {
        int t = -1;  //表示还没有确定
        for (int j = 1; j <= n; j++)//遍历 dist 数组,找到没有确定最短路径的节点中距离源点最近的点t
        {
            if (!state[j] && (t == -1 || dist[j] < dist[t]))  //
                t = j;
        }

        state[t] = 1;//state[i] 置为 1。

        for (int j = h[t]; j != -1; j = ne[j])//遍历 t 所有可以到达的节点 i
        {
            int i = e[j];
            dist[i] = min(dist[i], dist[t] + w[j]);//更新 dist[j]
        }


    }
}

int main()
{
    memset(h, -1, sizeof(h));//邻接表初始化

    cin >> n >> m;
    while (m--)//读入 m 条边
    {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
    }

    Dijkstra();
    if (dist[n] != 0x3f3f3f3f)//如果dist[n]被更新了,则存在路径
        cout << dist[n];
    else
        cout << "-1";
}

作者:Hasity
链接:https://www.acwing.com/solution/content/38318/
来源:AcWing

堆优化版的迪杰斯拉算法

如果说是一个稀疏图,比方说n是十万,那么如果两重循环,就会爆掉,然后要对它进行优化。

看一下朴素算法的时间复杂度:

for(i:1 ~ n)//n次
{
    t <- 没有确定最短路径的节点中距离源点最近的点;//每次遍一遍历dist数组,n次的复杂度是O(n^2)
    state[t] = 1;
    更新 dist;//每次遍历一个节点的出边,n次遍历了所有节点的边,复杂度为O(e)
}
算法的主要耗时的步骤是从dist 数组中选出:没有确定最短路径的节点中距离源点最近的点 t。只是找个最小值而已,没有必要每次遍历一遍dist数组。

在一组数中每次能很快的找到最小值,很容易想到使用小根堆。可以使用库中的小根堆(推荐)或者自己编写。

例题: Dijkstra求最短路 II

给定一个 n 个点 m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n,m≤1.5×10的5次方
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10的9次方。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>//堆的头文件

using namespace std;

typedef pair<int, int> PII;//堆里存储距离和节点编号

const int N = 1e6 + 10;

int n, m;//节点数量和边数
int h[N], w[N], e[N], ne[N], idx;//邻接表存储图
int dist[N];//存储距离
bool st[N];//存储状态

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});//插入距离和节点编号

    while (heap.size())  //while堆不空
    {
        auto t = heap.top();//取距离源点最近的点
        heap.pop();

        int ver = t.second, distance = t.first;//ver:节点编号,distance:源点距离ver 的距离

        if (st[ver]) continue;//如果距离已经确定,则跳过该点
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])//更新ver所指向的节点距离
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])  //j大于从t过来的距离
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});//距离变小,则入堆
            }
        }
    }

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

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);  //初始化成空结点
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    cout << dijkstra() << endl;

    return 0;
}

作者:Hasity
链接:https://www.acwing.com/solution/content/38323/
来源:AcWing

使用小根堆后,找到 t 的耗时从 O(n^2) 将为了 O(1)。每次更新 dist 后,需要向堆中插入更新的信息。所以更新dist的时间复杂度有 O(e) 变为了 O(e*logn)。总时间复杂度有 O(n^2) 变为了 O(n + e*longn)。适用于稀疏图。

总结

迪杰斯特拉算法适用于求正权有向图中,源点到其余各个节点的最短路径。注意:图中可以有环,但不能有负权边。

例如:如下图就不能使用迪杰斯特拉算法求节点 1 到其余各个节点的最短距离。
{:weith=150 height=150}

bellman-ford

贝尔曼ford算法的存储方式不一定需要写成邻接表,可以用最傻瓜式的算术方式,就开个结构体。

 求最短路的时候,如果有负权回路的话,那么最短路是不一定存在的,如果能求出来最短路,这个图里边是没有负权回路的。比方说想去某地旅游,比方说想从一号城市到n号城市旅游,但是这个飞机的航线没有从一到n的直飞的,但是可以中转,可以从一号城市先到二号城市,然后从二号城市到三号城市,然后再从三号城市到n号城市,就可以这样周转一下,然后每次周转,都有一个价钱。然后想求一下,从一号点到n号点最少需要多少钱,但是,每换乘一次,旅客的心情就会变差一些。所以可能会对做一个限制,就是最多只能换k次。所以这个问题其实是有实际含义的。

例题:有边数限制的最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1号点走到 n号点,输出 impossible

注意:图中可能 存在负权回路 。

输入格式

第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

点的编号为 1∼n。

输出格式

输出一个整数,表示从 1 号点到 n号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible

数据范围

1≤n,k≤500,
1≤m≤10000
1≤x,y≤n
任意边长的绝对值不超过 10000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510, M = 10010;

struct Edge { //使用结构体存储边,不用定义一大堆数组去加边
    int a;  //起点
    int b;  //终点
    int w;  //权重
} e[M];//把每个边保存下来即可
int dist[N];  //距离
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边

void bellman_ford() {
    memset(dist, 0x3f, sizeof dist); //dist初始化位无穷,八字节3f3f3f3f大于1e9
    dist[1] = 0;
    for (int i = 0; i < k; i++) {//k次循环
        memcpy(back, dist, sizeof dist);//备份,存一下上一次迭代的结果
        for (int j = 0; j < m; j++) {//遍历所有边,而dijkstra是遍历所有顶点n*n
            int a = e[j].a, b = e[j].b, w = e[j].w;
            dist[b] = min(dist[b], back[a] + w);
            //使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 0; i < m; i++) {  //读入m条边
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        e[i] = {a, b, w};
    }
    bellman_ford();
    if(dist[n]>0x3f3f3f3f/2)      puts("impossible");
    else printf("%d",dist[n]);
    return 0;
}

作者:松鼠爱葡萄
链接:https://www.acwing.com/solution/content/14088/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SPFA求最短路

spfa算法是对贝尔曼ford算法做一个优化。

spfa算法文字说明:

建立一个队列,初始时队列里只有起始点。

再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。

再建立一个数组,标记点是否在队列中。

队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。

重复执行直到队列为空。

在保存最短路径的数组中,就得到了最短路径。

例题: spfa求最短路

给定一个 n个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y的有向边,边长为 z。

输出格式

输出一个整数,表示 1号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible

数据范围

1≤n,m≤10的5次方
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2

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

const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列

void add(int a, int b, int c){//图中添加边和边的端点
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa(){
    q[++tt] = 1;//从1号顶点开始松弛,1号顶点入队
    dist[1] = 0;//1号到1号的距离为 0
    st[1] = 1;//1号顶点在队列中,防止存储重复的点
    while(tt >= hh){//不断进行松弛
        int a = q[hh++];//取对头记作a,进行松弛
        st[a] = 0;//取完队头后,a不在队列中了
        for(int i = h[a]; i != -1; i = ne[i])//更新a的所有邻边,遍历所有和a相连的点
        {
            int b = e[i], c = w[i];//获得和a相连的点和边
            if(dist[b] > dist[a] + c){//如果可以距离变得更短,则更新距离

                dist[b] = dist[a] + c;//更新距离

                if(!st[b]){//如果没在队列中
                    q[++tt] = b;//入队
                    st[b] = 1;//打标记
                }
            }
        }
    }
}
int main(){
    memset(h, -1, sizeof h);//初始化邻接表
    memset(dist, 0x3f, sizeof dist);//初始化距离
    int n, m;//保存点的数量和边的数量
    cin >> n >> m;
    for(int i = 0; i < m; i++){//读入每条边和边的端点
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);//加入到邻接表
    }
    spfa();
    if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达 
        cout << "impossible";
    else cout << dist[n];//否则能到达,输出距离
    return 0;
}

作者:Hasity
链接:https://www.acwing.com/solution/content/105508/
来源:AcWing

SPFA判断负环

例题:spfa判断负环

给定一个 n 个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你判断图中是否存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。

数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes

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

using namespace std;

const int N = 2e3 + 10, M = 1e4 + 10;

int n, m;
int head[N], e[M], ne[M], w[M], idx;
bool st[N];
int dist[N];
int cnt[N]; //cnt[x] 表示 当前从1-x的最短路的边数

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

bool spfa(){
    // 这里不需要初始化dist数组为 正无穷/初始化的原因是, 如果存在负环, 那么dist不管初始化为多少, 都会被更新

    queue<int> q;

    //不仅仅是1了, 因为点1可能到不了有负环的点, 因此把所有点都加入队列
    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 = head[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]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}

int main()
{
    cin >> n >> m;
    memset(head, -1, sizeof head);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    if (spfa()) {
        cout << "Yes" << endl;
    }
    else {
        cout << "No" << endl;
    }
    return 0;
}

作者:Bug-Free
链接:https://www.acwing.com/solution/content/42308/
来源:AcWing

Floyd求最短路

例题: Floyd求最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k个询问,每个询问包含两个整数 x 和 y,表示查询从点 x到点 y的最短距离,如果路径不存在,则输出 impossible

数据保证图中不存在负权回路。

输入格式

第一行包含三个整数 n,m,k。

接下来 m行,每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y表示询问点 x到点 y的最短距离。

输出格式

共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible

数据范围

1≤n≤200
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

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

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;   //Q为询问
int d[N][N];  //d是邻接矩阵,要处理的矩阵

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]);
}

int main()
{
    scanf("%d%d%d", &n, &m, &Q);

    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 (Q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);

        int t = d[a][b];
        //由于有负权边存在所以约大过INF/2也很合理
        if (t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }

    return 0;
}


作者:半瓶可乐
链接:https://www.acwing.com/solution/content/92654/
来源:AcWing

end

———————————————————————————————————————————

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值