最短路算法模板

稀疏图和稠密图的评判标准

规定点数为n,边数为m
稠密图: m ~ \(n^2\), m和\(n^2\)对标,采用邻接矩阵
稀疏图: m ~ n, m和n对标,采用邻接表
观察数据范围即可
对稠密图和稀疏图定义还是模糊的,没有一个确切的定义,但是使用邻接矩阵的空间复杂度为 \(O(n^2)\),邻接表为 \(O(m)\),找到最小的存储即可,有时还要根据算法来定。

算法区分

  1. dijkstra
  • 朴素版: 适合稠密图,因为复杂度只与点数相关,边多点也无所谓
  • 堆优化版: 适合稀疏图,因为复杂度和边数有关,边越少越好
  1. bellman_ford 和 spfa
    平均来说spfa的复杂度要优于bellman_ford,但bellman_ford适用于限定边数时的最短路问题

朴素版Dijkstra

算法思路:每次找到未确定最短路径的点中距离最小的点,用该点更新其他点的距离

为何Dijkstra不适用于负权边
Dijkstra是基于贪心的做法,贪心的正确性源于“能够确保局部最优解一定是全局最优解”。当图中不存在负权边时,可以确定与源点距离最小的点的当前距离一定是最小距离,因为后续再经过其他点只会让距离增大,不会减小。但是如果存在负权边,就有可能后续经过其他点使得距离更小。e.g.下图中的 A -> BA -> c -> B。设定A为起点,按照贪心的策略\(dis_B = -1\),实际最小距离\(dis_B = -4\)

/**
 * 问题是如何实现每次在未选择的点中找到距离最小的点
 * 如果用容器存储一下未选择的点可以,但是维护距离最小还是需要时间
 * 既然这样就没必要耗费那个空间了,每次直接遍历一遍所有点就行了,好在点的数量不是很多
 */
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;

int n, m;
int dis[N], vis[N];
int g[N][N]; //  因为复杂度和边数无关,所以它应用场景是稠密图,所以建图采用邻接矩阵

void dijkstra(int u)
{
    memset(dis, 0x3f, sizeof dis);
    dis[u] = 0;

    for (int i = 0; i < n; ++ i) // 每次选定一个点的最短路径,然后用这个点去更新其它点,n个点一共需要n次,因为这里的i只是用于计数所以并不强制要求从1开始
    {
        // 找到未选择点中路径长度最小的点
        int t = -1; // 存储我们要找的节点
        for (int j = 1; j <= n; ++ j)
            if (!vis[j] && (t == -1 || dis[t] > dis[j]))  // dis[t] > dis[j]是在t已经存储值后的写法,t有可能还没有存储
                t = j; // 此时的dis[t]就已经是最短距离了,后续的更新也不会更新它了(想要理解这个就必须理解Dijkstra的原理),所以如果只想找到到n点的最短路径长度,当t为n时后续的更新操作就可以不用进行了

        vis[t] = 1; // t已经选择过了

        // 用t去更新其它点的距离
        for (int j = 1; j <= n; ++ j)
            dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
}
int main()
{
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    while (m --)
    {
        int a, b, k;
        cin >> a >> b >> k;
        // 这里之所以只考虑重边并没有考虑自环是因为即使我们存储了自环,对于最短路的求解也不会产生什么影响
        g[a][b] = min(g[a][b], k);
    }

    dijkstra(1);
    
    // 图中两点之间并不一定存在路径,最简单的情况就是孤立点
    if (dis[n] == 0x3f3f3f3f) cout << -1 << endl;
    else cout << dis[n] << endl;

    return 0;
}

堆优化版Dijkstra

算法思路:将朴素版dijkstra中找最小点的操作使用小根堆进行优化,其余思想不变

/**
 * 朴素版Dijkstra耗费时间的地方在于“每次找到未选择点中具有最小距离值得点”都需要遍历一遍所有点,所以优化也就是对这个问题进行优化
 * 每次找到最小的值显然就是小根堆,有两种实现形式
 * 1.模拟小根堆
 * 2.priority_queue
 * 由于堆中存储的信息不能仅仅是距离值,因为我们需要找到距离最小的点来更新其它点的距离,所以还需要存储节它是第几个节点
 * 按照y总的意思,模拟堆的话我们就需要开额外的数组负责存储某个距离值对应的它是第几个节点,就需要使用最复杂那个模拟方式
 * 所以决定直接采用priority_queue来进行存储,使用pair同时存放两个信息
 * 
 * 如果采用模拟堆的方式,还有一个问题是距离和点的编号不是一一对应的关系,堆中存储的是距离,我们从堆中拿出距离最小的点,需要知道它对应的节点编号,但是可能存在多个距离相同的点,这里就找不到了
 * 
 * 堆只是起到了优化的作用,并不会代码的框架
 * 
 * 从数据范围可以看出是稀疏图,所以采用邻接表来进行存储
 */
#include <iostream>
#include <queue>
#include <cstring>

using namespace std;

typedef pair<int, int> PII; // first:距离 second:节点编号 之所以把距离放在第一位是因为我们希望根据距离进行排序,默认根据pair的首位进行排序

const int N = 2e5;

int n, m;
int dis[N], vis[N]; // 距离
int head[N], e[N], ne[N], w[N], idx; // 邻接表存储, 多出来的w是为了储存边权

void insert(int a, int b, int k)
{
    w[idx] = k;
    e[idx] = b;
    ne[idx] = head[a];
    head[a] = idx ++;
}
void dijkstra(int u) // 指定起始点为u时求到其它各点的最短路径长度
{
    memset(dis, 0x3f, sizeof dis);
    dis[u] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});

    while(!heap.empty())
    {
        // 直接获取未选择的点中距离最小的点
        auto t = heap.top();
        heap.pop();
        int ver = t.second;
        if (vis[ver]) continue; // 这一步的作用就是下面提到的新旧信息的问题
        vis[ver] = 1;

        for (int i = head[ver]; i != -1; i = ne[i])
        {
            int p = e[i];
            /** 
             * 在朴素版中我们找未选择并且距离最小的点是直接根据dis数组进行选择的,所以只需要更新dis数组即可找到最小值
             * 但是此时我们找这个点的方式改为从堆中进行寻找,所以我们此时仅更新dis数组会影响下次的寻找,所以按理说堆中的信息也应该进行更新
             * 但是stl中的队列是不允许进行随机访问的,所以我们没办法修改队列中pair的信息,所以我们采取的策略就是不管那个点的信息了,而是再push进去一个更新后的节点信息
             * 这样做的正确性在于我们push进去的点的信息中的距离值一定要比原来堆中同一点的距离值要小,所以在小根堆中一定排在前面,在找到这个点时一定会优先找到后来更新的信息,当这个点的距离确定后,下次再遇见这个点之前的信息就会直接跳过不管了,因为先访问到的信息一定是后来更新得到的
             */

            if (dis[p] > dis[ver] + w[i]) // 这里w[i]是指从t到p这条边的权值
            {
                dis[p] = dis[ver] + w[i];
                heap.push({dis[p], p});
            }
        }
    }
}
int main()
{
    cin >> n >> m;
    memset(head, -1, sizeof head); //链表初始化头节点
    while (m --)
    {
        int a, b, k;
        cin >> a >> b >> k;
        insert(a, b, k);
    }

    dijkstra(1);

    if (dis[n] == 0x3f3f3f3f) cout << -1 << endl;
    else cout << dis[n] << endl;
    return 0;
}

bellman-ford算法

算法思路:设题目要求为求经过k条边的最短路径长度,则遍历k次,每次遍历用所有边更新一下改变终点的距离值

/**
 * 外层循环的意思是遍历一次找到的就是从起点出发到其它点路径长度为1的最短路径
 * 遍历两次就是从起点到其他点路径长度为2的最短路径
 * 这就有点像离散数学里面关系矩阵乘法的含义了
 * 
 * 因为bellman_ford算法需要遍历所有边,所以直接使用结构体存边了
 */

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

using namespace std;

const int N = 510, M = 1e4 + 10;

struct Edge {
    int a, b, k; // 起点,终点,权值
}edges[M];

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

void bellman_ford(int u)
{
    memset(dis, 0x3f, sizeof dis);
    dis[u] = 0;
    
    for (int i = 0; i < k; ++ i) // 外层循环的含义为:遍历i次得到的dis[j]为从起点开始到j点经过i条边时的最短路径长度
    {
        memcpy(backup, dis, sizeof dis);
        for (int j = 0; j < m; ++ j) // 遍历所有边
        {
            Edge e = edges[j];
            dis[e.b] = min(dis[e.b], backup[e.a] + e.k);
            // dis[e.b] = min(dis[e.b], dis[e.a] + e.k); 
            //   1--->2-->3                                             
            //    \      /                                            
            //     \    /                                            
            //      \  /
            //       \/                                          
            // 1到2,2到3距离为1,1到3距离为3
            // 此时是第一次遍历
            // 考虑先遍历的是1到2,dis[2]的距离被更新为1
            // 之后遍历的是2到3,按照程序dis[3]的距离被更新为2,按理说我们是第一次遍历,找的是经过一条边的最短距离,dis[3]的正确结果应该为3,但是由于2先被更新导致3的结果出现错误
            // 所以这里为了解决这种问题把上一次遍历后的dis数组拷贝一份,在选取起点的dis时选择上一次遍历后的结果,这样就可以防止此次遍历前面节点的结果影响到后面节点
        }
    }

}
int main()
{
    cin >> n >> m >> k;
    for (int i = 0; i < m; ++ i)
    {
        int a, b, k;
        cin >> a >> b >> k;
        edges[i] = {a, b, k};
    }

    bellman_ford(1);

    /**
     * dijkstra中保证边权均为正值,所以我们定义的无穷远0x3f3f3f3f不会被更新,所以最终判定是否存在路径时,直接判断dis是否等于无穷远即可
     * 但是在bellman_ford算法中是存在负边权的,所以考虑这么一种情况,起点是孤立点,其余n-1个点在同一个联通分量中且边权均为负值,对于这个图实施bellman_ford算法
     * dis[终点]的值初始是无穷大,但是由于负权边的松弛操作,使得这个值不再等于0x3f3f3f3f,虽然意义仍是无穷远,但数值终究不再等于原来的无穷远
     * 极端情况为起点是孤立点,其余499个点在同一连通分量中,且边权均为最小值-10000,经过最多500次循环,dis[n]的值最小为0x3f3f3f3f - 500 * 10000(一次循环一条边,一条边就减少10000) = 1056109567(0x3f3f3f3f = 1061109567)
     * 如果存在一条从1到n的路径,那么这条路径最长为499 * 10000 = 4990000,所以说如果存在路径,dis[n]最大也就只能到4990000了,所以如果dis[n]大于这个数就代表是无穷远了
     * 
     * dis能够取到的值有两个范围,1是0~4990000, 2是1056109567~1061109567,只可能在这两个区间之内,只需要在这两个区间之间选定一个数来判断属于哪个区间即可,所以这里通过dis和inf / 2来判断是否是无穷大是正确的
     */
    if (dis[n] > 0x3f3f3f3f / 2) cout << "impossible" << endl;
    else cout << dis[n] << endl;

    return 0;
}

spfa算法

算法思路:实质为对bellman_ford的优化,只选择用更新距离后的点去更新其它点

/**
 * 这是我把bellman_fordk条边的限制改为n条边之后的程序
 * 不正确的原因在于spfa并没有要求一定是n-1条边得到最短路,我们限制了边的数目本身就不正确
 * 这也是为什么spfa不能包含负权回路,如果存在负权回路,spfa就永远得不到最终结果
 * 同时侧面说明了为什么bellman_ford可以包含负权回路,因为我们限制了边的数目,虽然负权回路可以一直减小路径长度,但是走的边数是限制的不可能一直走
 * 
 * spfa正确性证明是这样的:
 * 因为保证不存在负权回路,所以每一个点的最短路径是一定存在的
 * 而spfa运行过程中每一次把点放入队列都说明该点的最短路径值缩小了
 * 随着算法的执行,所有点的最短路径值不断减小,直到所有点的值都等于最短路径值,算法停止
 * 所以说保证该算法正确的关键为“保证所有点的最短路径一定存在”和“算法会使得所有点的最短路径值不断逼近并最终等于答案”
 * 
 * 和bellman_ford相比,spfa保证了每次用于更新的点都是对答案有用的点,所以spfa是对bellman_ford的优化
 * 
 * 一共n个点,如果某个点的最短路径长度为n说明存在负权回路,路径长度为n,说明有n+1个点,但一共有n个点,说明有两个点是相同的,即存在回路
 * 这个点的路径长度为n,进队一次路径长度更新一次,说明这个点至少进队了n次,为什么说是至少呢?
 * 因为前后两次更新该点的点的路径长度是相等的,更新后该点的路径长度相等但实际上是进队了2次,所以n次是最少的次数
 * 所以也有人根据点的进队次数来判断是否存在负权回路
 * 
 * spfa不再像bellman_ford一样必须遍历边了,所以不需要使用结构体存储边了,根据数据范围采用邻接表存储即可
 */
 
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int dis[N];
bool vis[N];
int head[N], e[N], ne[N], w[N], idx;

/**
 * 为什么需要vis判断当节点a在队列中时不再进入一次
 * 考虑这样一种情况,此时队列中有2个节点b 和 c,他们都可以更新a节点的值
 * 当用b节点更新a节点后会把a进队,用c更新a节点距离后还有没必要让它再进队一次
 * 答案是没有,假设我们在b和c更新完a节点后让a进入两次,轮到a时,第一次的a已经完成了我们想让它做的事情,第2次也会执行一遍,但是完全没有必要
 * 或者换一种想法,假设我们就让两个a同时入队了,那么当后续再对a发生更新操作时,两个a的信息是保持同步的,用第一个a和第2个a进行松弛操作是完全一样的,没必要进行两次
 */

void insert(int a, int b, int k)
{
    w[idx] = k;
    e[idx] = b;
    ne[idx] = head[a];
    head[a] = idx ++;
}
void spfa()
{
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    queue<int> q;
    q.push(1);
    vis[1] = true; // 保证在同一时间同一点不会同时进入队列,不代表每个点只进入队列一次。两次使用相同距离的相同点进行松弛操作是冗余的
    
    while (q.size())
    {
        int t = q.front();
        q.pop();
        vis[t] = false; // 说明不是每个点只进队一次
        
        for (int i = head[t]; i != -1; i = ne[i])
        {
            int p = e[i];
            if (dis[p] > dis[t] + w[i])
            {
                dis[p] = dis[t] + w[i];
                if (!vis[p]) 
                {
                    q.push(p);
                    vis[p] = true;
                }
            }
        }
    }
}
int main()
{
    memset(head, -1, sizeof head);
    cin >> n >> m;
    while (m --)
    {
        int a, b, k;
        cin >> a >> b >> k;
        insert(a, b, k);
    }
    
    spfa();
    
    if (dis[n] == 0x3f3f3f3f) cout << "impossible" << endl; // spfa优化的位置恰好在于去掉了无效的答案更新操作,所以无穷远的点是不会进入队列的,无穷远这个值也不会发生改变
    else cout << dis[n] << endl;
    
    return 0;
}

/**
 * 循环队列实现
 * 如果采用数组模拟队列实现spfa,由于每个点可能入队多次所以我们无法确定数组长度,实际上这种并非真正溢出而是假溢出
 * 但在spfa中我们保证同一时刻同一个点只会在队列中出现一次,多次出现进行松弛操作是冗余的
 * 所以采用循环队列可以解决假溢出的问题
 * 需要注意代码中循环队列的实现方式
 */
#include <iostream>
#include <cstring>
#include <cstring>
#include <cstdio>

using namespace std;

const int N = 2510, M = 6200 * 2 + 10;

int n, m, s, t;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int dis[N];
int q[N], hh, tt; // 保证每个点同一时刻只在队列中出现一次,所以大小为N即可

void add(int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
void spfa()
{
    int hh = 0, tt = 0; // tt:尾后指针,方便当tt到达尾部时向头部的指向
    memset(dis, 0x3f, sizeof dis);
    dis[s] = 0;
    st[s] = true; // 确保一个点只在队列中出现一次,两次使用相同距离的相同点进行松弛操作是冗余的
    q[tt ++] = s;
    
    while(hh != tt) // 注意这里必须是!=,而不能是<,因为在循环队列中hh可能大于tt但是队列不为空
    {
        int t = q[hh ++];
        if (hh == N) hh = 0;
        st[t] = false;
        
        for (int j = h[t]; j != -1; j = ne[j])
        {
            int p = e[j];
            if (dis[t] + w[j] < dis[p])
            {
                dis[p] = dis[t] + w[j];
                if (!st[p]) 
                {
                    q[tt ++] = p;
                    if (tt == N) tt = 0;
                    st[p] = true;
                }
            }
        }
    }
    
}
int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m >> s >> t;
    for (int i = 0; i < m; ++ i)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    
    spfa();
    
    cout << dis[t] << endl;
    
    return 0;
}

spfa判断负权回路

一般情况
在除了ACM以外的比赛中,使用队列进行spfa即可
特殊情况
有些情况下,由下图可得,使用队列可能会遍历很多无关点,从而导致TLE。有两种解决方法:

  • 根据队列和栈数据进出的顺序不同,采用栈来实现,虽然效率并不稳定,但是一般情况下的表现都不错,由下图可得使用栈时一旦遇到一个负环上的点,那么定位到一个负环的几率要大于队列,所以找到负环的时间也就一定程度上减少了。同时需要注意,这样做可以减少运行时间,但是如果还是TLE,把STL的stack转换为数组模拟的会更快。
  • 当一个点的更新次数足够多时,大概率可以相信图中存在负环,添加一个trick判断,及时结束spfa避免TLE。此种方法比较玄学,没有明确的数学证明。

[注]:有些情况下, 使用栈未必会快于队列,这两者的运行速度对比并没有经过严格证明。当然一般情况下使用队列是没有问题的,栈只是队列出问题时一种选择。

/**
 * SPFA算法求负权回路的依据
 * 首先说明一个事实,n个点,假设一定存在最短路径,边数最多为n-1
 * 一共有n个点,假设某个点的最短路径边数为n,说明应该有n+1个点,但一共就n个点,所以一定有两个点是同一个点,也就是一定存在一个圈,而且既然存在这个圈,说明这个圈一定是负权,否则怎么可能会多走这么一圈
 * 每进队一次,路径的边数就会在更新它的点的基础上+1,所以该点最少进队次数为n,为什么是最少,考虑这么一种情况,b和c同时更新了a点,但b和c的路径边数是相等的,所以a的路径边数虽然+1,但进队是两次
 */
 
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int dis[N], cnt[N];
bool vis[N];
int head[N], e[M], ne[M], w[M], idx;

void insert(int a, int b, int k)
{
    w[idx] = k;
    e[idx] = b;
    ne[idx] = head[a];
    head[a] = idx ++;
}
bool spfa()
{
    /**
     * 这里有两点要说明
     * 1.dis数组不需要初始化
     * 2.当我们不确定图中结构时,需要让所有点入队,而非仅1号点
     *   如果能够确定图中某个点可以走到其它所有点,那么只需将该点入队,无需让所有点入队
     * 
     * 1.这里我们只是判断一个点的路径边数是不是为n,或者说它是不是会被更新n次,初始值无论是无穷大还是0,遇到负边权都会更新,遇到正边权都比不会更新,所以初始化就无所谓了
     * 2.不确定图中结构时,某个点可能是孤立点,如果仅从该点开始遍历图,会导致无法检测出负权回路,所以要从所有点都开始一遍
     *   而如果我们能够确定从某个点开始一定可以走到其它点,那么从该点开始也能够保证找到应有的负权回路,就无需从所有点都开始一次spfa
     */
    queue<int> q;
    // stack<int> q; // 使用栈时替换为该语句
    for (int i = 1; i <= n; ++ i)
    {
        q.push(i);
        vis[i] = true;
    }

    int count = 0; // trick计数器
    while (q.size())
    {
        int t = q.front();
        // int t = q.top(); // 使用栈时替换为该语句
        q.pop();
        vis[t] = false;
        
        for (int i = head[t]; i != -1; i = ne[i])
        {
            int p = e[i];
            if (dis[p] > dis[t] + w[i])
            {
                dis[p] = dis[t] + w[i];
                cnt[p] = cnt[t] + 1; // 路径边数
                
                if (++ count > 10000) return true; // trick判断
                if (cnt[p] >= n) return true;
                if (!vis[p]) 
                {
                    q.push(p);
                    vis[p] = true;
                }
            }
        }
    }
    
    return false;
}
int main()
{
    memset(head, -1, sizeof head);
    cin >> n >> m;
    while (m --)
    {
        int a, b, k;
        cin >> a >> b >> k;
        insert(a, b, k);
    }
    
    if (spfa()) cout << "Yes" << endl;
    else cout << "No" << endl;
    
    return 0;
}

Floyd算法

算法思路
每次选择一个中间节点,用以更新任意两点间的距离,直到把所有节点都作为一次中间节点
代码实现

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

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;

int n, m, k;
int g[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)
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
int main()
{
    cin >> n >> m >> k;

    /**
     * 这里为什么需要初始化?
     * 朴素版dijkstra同样使用邻接矩阵存储,同样存在重边和自环,但当时并没有初始化自己到自己距离为0
     * 首先自己到自己的最短路径肯定是0,由于dijkstra使用dis数组存储这个距离,我们初始化了dis[1]为0,虽然g中1到1可能不为0,但是最终的dis[1]还是0,结果是正确的
     * 但是这里我们的答案也是存储在g中的,如果不初始化g[i][i]为0,最终的结果就是错的
     */
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= n; ++ j)
            if (i == j) g[i][j] = 0;
            else g[i][j] = INF;
    
    while (m --)
    {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z);
    }

    floyd();

    while (k --)
    {
        int x, y;
        cin >> x >> y;
        /**
         * 和bellman_ford一样,同样可能出现无穷远的节点用负边权去更新另一个无穷远的节点,无穷远的值可能会减小
         * 其实只有存在负边权时才有可能出现这个问题,所以dijkstra根本不会存在
         * 而spfa将bellman_ford胡乱更新改为每次更新均为有效更新避免了这种问题
         * 所以剩下的bellman_ford和floyd都需要解决这个问题
         * 
         * 我们选的分界值必须满足 >合法时dis的最大值,并且<非法时dis最小值
         * 非法:对于c来说,无穷远节点a去更新无穷远节点b,这是很荒谬的一件事,c本身就无法到达a点,但是由于a到b的边权为负,
         * b的距离值是会被更新的,但显然c无法通过a点到达b点,所以这种更新就叫做非法更新
         * 极端情况为1号点为孤立点,其余199个点在一个连通分量中,且那199个点之间的边权均为最小值-10000,经过连通分量中198点的非法更新
         * 从1号点到那198个点的距离最小值就是到n号点的,因为n号点经过了最多次的更新,减少了198 * -10000,也就是非法更新情况下无穷远的最小值为1059129567
         * 讲实话,此时这个不是很好理解了,因为边权有范围,所以最多也不会在无穷远的基础上修改非常多
         * 
         * 合法情况下距离的最大值应该就是199 * 10000了,此时就是最多200个点199条边,每条边都是10000,从1号点到n号点的距离199 * 10000就是最大距离了
         * 
         * 所以根据530554783是可以判定为无穷远的
         */
        if (g[x][y] > INF / 2) cout << "impossible" << endl;
        else cout << g[x][y] << endl;
    }
    return 0;
}

算法应用

  1. 求最短路
  2. 恰好经过k条边的最短路
  3. 求传递闭包
    传递闭包是离散数学中的概念。对于算法竞赛中的传递闭包问题通俗地说是指在交际网络中,给定若干个元素和若干对二元关系,且这些关系具有传递性,通过这些传递性推导出尽量多的元素之间的关系的问题叫做传递闭包问题。
    可以将传递闭包问题转化为图论问题,元素看为点,关系看为边,每两个存在关系的元素之间存在一条边,使用Floyd算法即可求出任意两个元素之间的关系。
    一道例题
  4. 最小环问题

双端队列bfs

应用条件

  1. 指定起点和终点(不同于单源最短路,单源最短路是一个起点多个终点即多条路径,本算法只能是一条路径)
  2. 图中边权只能包含两种(例如长度为0和1)

同dijkstra算法相比的优点
dijkstra算法是可以满足以上两个限定条件的,但当点数过多时,dijkstra算法是容易TLE的
这是因为,当题目给定的是一个棋盘,并非直接指定所有的边时,建图时我们需要为棋盘上的每个点映射为一个编号,这样才能构建出邻接矩阵或邻接表
而bfs则无需完成这项工作,能够节省一些时间

示例题目
电路维修
这道题目更像是用BFS完成了一遍Dijkstra,只不过BFS用的不是一般队列而是双端队列。
因为从代码实现的角度来看,很多逻辑都不是BFS所能想到的,都是依托的Dijkstra算法

posted @ 2021-01-27 08:12  0x7F  阅读(209)  评论(0编辑  收藏  举报