3.2 搜索与图论 | Dijkstra、Bellman-Ford、SPFA、Floyd

3.2 搜索与图论 | Dijkstra、Bellman-Ford、SPFA、Floyd

这是我的一个算法网课学习记录,道阻且长,好好努力

最短路

MINDMAP

重在建图 侧重实现的思路

a —w—> b (a、b为两个点,w为权重)

数据结构中对于稀疏图的定义为:有很少条边或弧(边的条数 |E| 远小于 |V|² )的图称为稀疏图(sparse graph),反之边的条数 |E| 接近 |V|² ,称为稠密图(dense graph)。

单从算法效率上讲,稀疏图与稠密图的分界点大概就在 m = n² / log n 处,但是实际上复杂度是有系数的,所以单从式子上计算也是不太科学的,可以作为一个参考。

朴素的 Dijkstra 算法

最朴素的思想就是按长度递增的次序产生最短路径。即每次对所有可见点的路径长度进行排序后,选择一条最短的路径,这条路径就是对应顶点到源点的最短路径。

步骤:

  1. 不断运行广度优先算法找可见点,计算可见点到源点的距离长度

  2. 从当前已知的路径中选择长度最短的将其顶点加入S作为确定找到的最短路径的顶点。

Dijkstra不允许存在负权边

时间复杂度O(n^2)

例题:AcWing 849. Dijkstra求最短路 I

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

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

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

using namespace std;

const int N = 510;

int n, m;
int g[N][N]; // g[a][b]存储a到b的距离
int dist[N]; // 从1号点走到n号点当前最短距离是多少
bool st[N]; // 最短路是否确定

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist); // 将距离初始化为正无穷
    dist[1] = 0;

    // 循环n-1次(n个节点,n-1次距离)
    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])) // 如果j点距离未定 且 未更新或这个距离不是最短的
                t = j;
        // if (t == n) break;

            st[t] = true; // t的距离确定

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

    if (dist[n] == 0x3f3f3f3f) return -1; // 如果距离无穷大,说明不存在这条路径,返回-1

    return dist[n]; // 返回最短距离
}

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

    memset(g, 0x3f, sizeof g);

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c); // 处理重边
    }

    int t = dijkstra();

    printf("%d\n", t);

    return 0;
}

堆优化版朴素的 Dijkstra 算法

  • 堆有两种实现方式:手写堆和优先队列

时间复杂度O(m log n)

priority_queue<Type, Container, Functional>

Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。

如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator,也就是优先队列是大顶堆,队头元素最大。

例题:AcWing 850. Dijkstra求最短路 II

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

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

#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; // 邻接表储存图 w表示权重
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); // 所有距离初始化为INFINITE
    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; // ver表示点的编号
        if (st[ver]) continue; // 判断该点是否为冗余备份 如果是 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];
}

int main()
{
    memset(h, -1, sizeof h); // 初始化成空节点
    cin >> n >> m;
    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    cout << dijkstra() << endl;

    return 0;
}

Bellman-Ford 算法

时间复杂度O(nm)

Bellman-Ford算法适用于含有负权边但不含负环的情况

dijkstra 不能解决负权边是因为 dijkstra要求每个点被确定后 st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了

步骤:

  1. 初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。

  2. 进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。

松弛操作

以a为起点,b为终点,ab边长度为w为例。dis[a]代表源点s到a点的路径长度,dis[b]代表源点s到b点的路径长度。

如果满足dist[b] <= dist[a] + w(三角不等式) 则将dis[b]更新为dis[a] + w。

一般是没有负权回路的

为了不发生串联,更新时候只用上一次的结果,需要备份

例题:AcWing 850. Dijkstra求最短路 II

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

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

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

using namespace std;

const int N = 510, M = 10010;

int n, m, k;
int dist[N], backup[N];

struct Edge
{
    int a, b, w;
}edges[M];

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

    // 题目要求不超过n条边
    for (int i = 0; i < k; i ++ )
    {
        memcpy(backup, dist, sizeof dist); // 备份
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    
    if (dist[n] > 0x3f3f3f3f / 2) return -1; // 判断是否存在负权 
    
    return dist[n];
}

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

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

    int t = bellman_ford();

    if (t == -1) puts("imposible");
    else printf("%d\n", t);

    return 0;
}

SPFA 算法

时间复杂度一般O(m)最坏O(nm)`

其实是队列优化的 bellman-ford 算法。

迭代的时候用一个队列

实现方法:

建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

判断有无负环:
  如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

这个算法也适用于解决部分使用 Dijkstra 算法的题目;可以解决有负权的问题,也可以解决没有负权的问题,时间可能是优于Dijkstra算法的,适用性和实用性都很好,但是可能会被卡。

例题1:AcWing 851.spfa求最短路

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

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

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

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

using namespace std;

const int N = 100010;

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 spfa()
{
    memset(dist, 0x3f, sizeof 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;

        for (int i = h[t]; i != -1; i = ne[i]) // 用t这个点来更新
        {
            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;
                }
            }
        }
    }

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

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

例题2:AcWing 851.spfa判断负环

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你判断图中是否存在负权回路。

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

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[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 spfa()
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }

    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 (cnt[j] >= n) return true; // 超过n 则存在负环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

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

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}

Floyd 算法

“一个一个多点的小涟漪,最后小涟漪铺满整个水面。”

一个一个的点更新邻接矩阵中的距离。

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

例题:AcWing 854.Floyd求最短路

给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输"impossible”。
数据保证图中不存在负权回路。

#include <iostream>

using namespace std;

const int N = 110, INF = 1e9;

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 ++ )
                d[i][j] = min(d[i][j], d[i][j] + 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;
        cin >> a >> b >> c;

        d[a][b] = min (d[a][b], c); // 保留最小的边
    }

    floyd();

    while (k -- )
    {
        int a, b;
        cin >> a >> b;
        if (d[a][b] > INF / 2) cout << "imposible" << endl;
        else cout << d[a][b];
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值