图论——最短路问题

图论------最短路问题

  • 单源最短路
    • dijistra算法
      • [P3371 【模板】单源最短路径(弱化版) - 洛谷](#P3371 【模板】单源最短路径(弱化版) - 洛谷)
    • dijkstra堆优化
      • [P4779 【模板】单源最短路径(标准版) - 洛谷](#P4779 【模板】单源最短路径(标准版) - 洛谷)
    • [bellman-ford 算法](#bellman-ford 算法)
      • [P3371 【模板】单源最短路径(弱化版) - 洛谷](#P3371 【模板】单源最短路径(弱化版) - 洛谷)
    • SPFA
      • [P3371 【模板】单源最短路径(弱化版) - 洛谷](#P3371 【模板】单源最短路径(弱化版) - 洛谷)
      • [P3385 【模板】负环 - 洛谷](#P3385 【模板】负环 - 洛谷)
        • [ford 算法判断负环](#ford 算法判断负环)
        • [SPFA 判断负环](#SPFA 判断负环)
    • 单源最短路总结和OJ参考
      • [P1629 邮递员送信 - 洛谷](#P1629 邮递员送信 - 洛谷)
      • [P1744 采购特价商品 - 洛谷](#P1744 采购特价商品 - 洛谷)
      • [P2136 拉近距离 - 洛谷](#P2136 拉近距离 - 洛谷)
      • [P1144 最短路计数 - 洛谷](#P1144 最短路计数 - 洛谷)
  • 多源最短路
    • floyd算法
      • [B3647 【模板】Floyd - 洛谷](#B3647 【模板】Floyd - 洛谷)
      • [P2910 Clear And Present Danger S - 洛谷](#P2910 Clear And Present Danger S - 洛谷)
      • [P1119 灾后重建 - 洛谷 理解floyd本质](#P1119 灾后重建 - 洛谷 理解floyd本质)
      • [P6175 无向图的最小环问题 - 洛谷](#P6175 无向图的最小环问题 - 洛谷)
  • OJ参考

单源最短路

在图 G G G 中,假设 v i v_i vi 和 v j v_j vj 为图中的两个顶点,那么 v i v_i vi 到 v j v_j vj 路径上所经过边的权值之和就称为带权路径长度。

由于 v i v_i vi 到 v j v_j vj 的路径可能有多条,将带权路径长度最短的那条路径称为最短路径。

最短路径一般分为两类:

  • 单源最短路径,即图中一个顶点到其它各顶点的最短路径。
  • 多源最短路径,即图中每对顶点间的最短路径。

如下图所示,研究从家到达其他地方的最短路径就是单源最短路问题,研究任意两个小区之间的距离是多源最短路问题。

dijistra算法

dijistra 算法是基于贪心 思想的单源最短路算法 ,求解的是 "非负权图" 上单源最短路径。

dijistra 算法的证明可用数学归纳法或反证法。

例如下图从 1 到 4 的最短路是 3 。假设不是 3 ,则肯定能从其他的路找到比 3 小的数,但找不到,所以从 1 到 4 的最短路是 3 。

常规版 dijistra 算法流程:

  • 准备工作:

    • 创建一个长度为 n n n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路径;
    • 创建一个长度为 n n n 的 bool 数组 vis,其中 vis[i] 表示 i 点是否已经确定了最短路径。
  • 初始化:dist[start] = 0,其余结点的 dist 值为无穷大,表示还没有找到最短路径。

  • 重复:在所有没有确定最短路径的点中,找出最短路径长度最小的点 u。打上确定最短路径的标记,然后对 v 的出边进行松驰操作;

  • 重复上述操作,直到所有点的最短路径都确定。

松弛操作 :对于一条从顶点 u u u 到 v v v、长度为 w w w 的连边,如果 d i s u + w < d i s v dis_u + w < dis_v disu+w<disv,那么就可以将 d i s v dis_v disv 的值变成 d i s u + w dis_u + w disu+w,因为这代表着从起点 s s s 走到顶点 u u u,经过这条边再走到顶点 v v v,是一条未发现过的且比原先的最优路径还要短的边。这样的操作被称为松弛操作。由于 s = 1 s = 1 s=1,所以显然 d i s 1 = 0 dis_1 = 0 dis1=0 且不会再变化。于是就可以用 1 号点的所有出边来进行松弛并将 1 标记,代表这个点的 d i s dis dis 值将不再变化。

dijstra 算法对于无向图和有向图均适用,只要图中没有负环就行。

dijstra 邻接矩阵实现参考。这个参考能过 P3371 【模板】单源最短路径(弱化版) - 洛谷 大部分测试样例,但作者设置了内存上限使得这个题不能用邻接矩阵。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vvll = vector<vll>;
using vb = vector<bool>;

const LL INF = (1LL << 31) - 1;

void dijistra(vvll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    dist[start] = 0;
    for (LL i = 1; i < dist.size() - 1; i++) {
        // 找出没有确定最短路的点中,最小的点
        int aim = 0;
        for (LL j = 1; j < dist.size(); j++) {
            if (vis[j])
                continue;
            if (dist[j] < dist[aim])
                aim = j;
        }
        // 标记,松弛操作
        vis[aim] = 1;
        for (LL j = 1; j < pct[aim].size(); j++)
            if (dist[j] > dist[aim] + pct[aim][j])
                dist[j] = dist[aim] + pct[aim][j];
    }
    for (LL i = 1; i < dist.size(); i++)
        cout << dist[i] << ' ';
}

int main() {
    LL n, m, s;
    vvll pct;
    cin >> n >> m >> s;
    pct.resize(n + 1, vll(n + 1, INF));
    for (LL i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s][e] = min(pct[s][e], w);// 防止重边
    }
    dijistra(pct, s);
    return 0;
}

P3371 【模板】单源最短路径(弱化版) - 洛谷

P3371 【模板】单源最短路径(弱化版) - 洛谷

dijstra 算法的邻接表实现:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;

const LL INF = (1LL << 31) - 1;

void dijistra(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    dist[start] = 0;
    for (int tmpn = 1; tmpn < pct.size() - 1; tmpn++) {
        LL aim = 0;
        for (int i = 1; i < dist.size(); i++) {
            if (vis[i])
                continue;
            if (dist[i] < dist[aim])
                aim = i;
        }
        vis[aim] = 1;
        for (auto &x : pct[aim])
            if (dist[x.first] > dist[aim] + x.second)
                dist[x.first] = dist[aim] + x.second;
    }
    for (int i = 1; i < dist.size(); i++)
        cout << dist[i] << ' ';
    cout << '\n';
}

int main() {
    LL n, m, s;
    vvpll pct;
    cin >> n >> m >> s;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w}); // 邻接表的松弛操作可无视重边
    }
    dijistra(pct, s);
    return 0;
}

dijistra 的链式前向星实现就是把邻接表的 dijistra 的遍历邻接表的过程变成遍历单链表。这里省略。

dijkstra堆优化

  • 准备工作:

    • 创建一个长度为 n n n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路径;
    • 创建一个长度为 n n n 的 bool 数组 vis,其中 vis[i] 表示 i 点是否已经确定了最短路径;
    • 创建一个小根堆,维护更新后的结点。(也就是需要确定最短路径的结点)
  • 初始化:dist[1] = 0,然后将 {0, s} 加到堆里,其余结点的 dist 值为无穷大,表示还没有找到最短路径。

  • 重复:弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有标记过,打上标记,进行松驰操作。

  • 重复上述操作,直到堆里面没有元素为止。

如果出现负权边,就会出错,无论如何优化都是如此。例如这个图:

这里 1 到 3 的最短路径长是 -3 ,使用 dijistra 算法就会出错。

P4779 【模板】单源最短路径(标准版) - 洛谷

P4779 【模板】单源最短路径(标准版) - 洛谷

这个题采用大量的稠密图,使得 O ( n 2 ) \text{O}(n^2) O(n2) 的 dijistra 算法超时。需要使用堆优化的 dijistra 算法。

因数据量庞大,邻接矩阵注定用不了,需要使用邻接表或链式前向星。

这里使用邻接表。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

void dijstra(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    priority_queue<pll, vpll, greater<pll>> pq;
    dist[start] = 0;
    pq.push({0, start}); //{权值,结点}
    while (!pq.empty()) {
        pll np = pq.top();
        pq.pop();
        if (vis[np.second]) // 存在重边的情况
            continue;       // 队列里拿出的点可能已经求出了最短路
        vis[np.second] = 1;
        for (auto &x : pct[np.second]) 
            if (dist[x.first] > dist[np.second] + x.second) {
                dist[x.first] = dist[np.second] + x.second;
                pq.push({dist[x.first], x.first});
            }
    }
    for (int i = 1; i < dist.size(); i++)
        cout << dist[i] << ' ';
}

int main() {
    LL n, m, s;
    vvpll pct;
    cin >> n >> m >> s;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
    }
    dijstra(pct, s);
    return 0;
}

这个实现的 dijistra 算法的时间复杂度就是 O ( m log ⁡ m ) \text{O}(m\log m) O(mlogm) ,因为堆维护的是边,遍历邻接表时也是遍历边,其他的还有 distvis 的初始化和点有关,相对与 m m m 比较小,可近似忽略。

bellman-ford 算法

Bellman-Ford 算法 ( 简称 BF 算法 ) 是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路 ,并可以对最短路不存在的情况进行判断

最短路不存在的情况:负环中的负边权绝对值过大,使得最短路可以无限减小。

算法核心思想:不断尝试对图上每一条边进行松弛,直到所有的点都无法松弛为止。因此可以选择不存图,而是存边。

Bellman-Ford 算法流程:

  • 准备工作

    • 创建一个长度为 n n n 的 dist 数组,其中 dist[i] 表示从起点到 i i i 结点的最短路径。
  • 初始化
    dist[1] = 0 ,其余结点的 dist 值为无穷大,表示还没有找到最短路径。

  • 重复:每次都对所有的边进行一次松弛操作。

  • 重复上述操作,直到所有边都不需要松弛操作为止。

在最短路径存在的情况下,由于一次松弛操作会使最短路的边数至少增加 1,而最短路的边数最多为 n − 1 n - 1 n−1。因此整个算法最多执行轮松弛操作 n − 1 n - 1 n−1 轮。故总时间复杂度为 O ( n m ) \text{O}(nm) O(nm)。比朴素的 dijistra 算法慢,因为存在边的数量很多的情况,且没有使用优化手段。

P3371 【模板】单源最短路径(弱化版) - 洛谷

P3371 【模板】单源最短路径(弱化版) - 洛谷 参考程序:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

void ford(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    dist[start] = 0;
    bool flag = 0;
    for (int times = 1; times < dist.size() - 1; times++) {
        flag = 0;
        for (int i = 1; i < dist.size(); i++) {
            if (dist[i] == INF) // 起点到当前顶点无通路时跳过
                continue;
            for (auto &x : pct[i]) {
                if (dist[x.first] > dist[i] + x.second) { // 松弛操作
                    dist[x.first] = dist[i] + x.second;
                    flag = 1;
                }
            }
        }
        if (!flag) // 遍历边时无松弛操作即可停止
            break;
    }
    for (int i = 1; i < dist.size(); i++)
        cout << dist[i] << ' ';
}

int main() {
    LL n, m, s;
    vvpll pct;
    cin >> n >> m >> s;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
    }
    ford(pct, s);
    return 0;
}

SPFA

SPFA 即 Shortest Path Faster Algorithm,本质是用队列对 BF 算法做优化,在形式上很像 bfs 。在国际上统称为 "队列优化的 Bellman-Ford 算法" ,仅在中国大陆流行 " SPFA 算法" 的称谓。

SPFA 的思路是在 BF 算法中,很多时候并不需要那么多无用的松弛操作:

  • 只有上一次被松驰的结点,它的出边,才有可能引起下一次的松弛操作;
  • 因此,如果队列来维护"哪些结点可能会引起松弛操作",就能只访问必要的边了,时间复杂度就能降低。

SPFA 算法流程:

  • 准备工作

    • 创建一个长度为 n n n 的 dist 数组,其中 dist[i] 表示从起点到 i i i 结点的最短路径;
    • 创建一个长度为 n n n 的 bool 数组 vis,其中 vis[i] 表示 i i i 点是否已经在队列中。
  • 初始化 :标记 dist[1] = 0,同时 1 1 1 入队;其余结点的 dist 值为无穷大,表示还没有找到最短路径。

  • 重复 :每次拿出队头元素 u u u,去掉在队列中的标记,同时对 u u u 所有相连的点 v v v 进行松弛操作。如果结点 v v v 被松弛,那就放进队列中。

  • 重复上述操作,直到队列中没有结点为止。

虽然在大多数情况下 SPFA 跑得很快,时间复杂度是 O ( k m ) \text{O}(km) O(km) ( k k k 是常数) ,但其最坏情况下的时间复杂度为 O ( n m ) \text{O}(nm) O(nm)。将其卡到这个复杂度也是不难的,只需要让所有的结点都和指定某个结点之间都存在边,只要任意一个结点和这个结点之间产生了松弛操作,所有结点都要遍历一遍。所以在没有负权边时最好使用 (堆优化) dijistra 算法。

P3371 【模板】单源最短路径(弱化版) - 洛谷

P3371 【模板】单源最短路径(弱化版) - 洛谷 参考程序:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

void spfa(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0); // 标记结点是否在队列中
    queue<LL> q;
    q.push(start);
    vis[start] = 1;
    dist[start] = 0;
    while (!q.empty()) {
        LL np = q.front();
        q.pop();
        vis[np] = 0; // 出队则解除标记
        for (auto &x : pct[np])
            if (dist[x.first] > dist[np] + x.second) {
                dist[x.first] = dist[np] + x.second;
                if (vis[x.first]) // 在队列中则不必反复入队
                    continue;
                vis[x.first] = 1;
                q.push(x.first);
            }
    }
    for (int i = 1; i < dist.size(); i++)
        cout << dist[i] << ' ';
}

int main() {
    LL n, m, s;
    vvpll pct;
    cin >> n >> m >> s;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
    }
    spfa(pct, s);
    return 0;
}

P3385 【模板】负环 - 洛谷

P3385 【模板】负环 - 洛谷

负环使 dijistra 这位伟大的天骄夭折于此,但却给 ford 算法和 spfa 开了一扇门。

例如测试样例给的负环:

这个负环不存在单源最短路。

若使用 ford 运行这个图,执行完 n − 1 n-1 n−1 次松弛操作时,第 n n n 次必定不会进行,除非它是负环,使得松弛操作可以永远进行下去。

ford 算法判断负环

P3385 【模板】负环 - 洛谷

ford 算法判断负环的参考程序:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

bool ford(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    dist[start] = 0;
    bool flag = 0;
    for (int times = 1; times < dist.size(); times++) { // 遍历n轮
        flag = 0;
        for (int i = 1; i < dist.size(); i++) { // 遍历所有的边
            if (dist[i] == INF)
                continue;
            for (auto &x : pct[i]) {
                if (dist[x.first] > dist[i] + x.second) {
                    dist[x.first] = dist[i] + x.second;
                    flag = 1;
                }
            }
        }
        if (!flag)
            break; // 没有松弛操作时
    }
    return flag;
}

void ac() {
    LL n, m;
    vvpll pct;
    cin >> n >> m;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
        if (w >= 0)
            pct[e].push_back({s, w});
    }
    if (ford(pct, 1))
        cout << "YES\n";
    else
        cout << "NO\n";
}

int main() {
    LL T = 1;
    cin >> T;
    while (T--)
        ac();
    return 0;
}
SPFA 判断负环

P3385 【模板】负环 - 洛谷

SPFA 省去了很多不必要的松弛操作,但总有一些测试样例构建的图,使 SPFA 和 ford 算法的时间复杂度相同。

传统的 SPFA 以队列为空作为结束标志,所以当出现负环时,程序进入死循环,所以在传统 SPFA 的基础上为每个结点加一个计数器记录从起点走到当前顶点时一共经过了多少条边 。因为 SPFA 从起点到终点,遍历的边数不超过 n − 1 n-1 n−1 , n n n 是结点数,但若存在负环就有超过 n − 1 n-1 n−1 的可能。

这个思路相当于使用动态规划 辅助 SPFA 判断负环,对有向图来说,转移方程 dp[end]=dp[start]+1dp[i] 表示从起点到 i 经历的边数,初始话为0即可,当产生松弛操作时进行填表。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

bool spfa(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0); // 标记结点是否在队列中
    vll dp(pct.size(), 0); // 计数器,辅助spfa判断是否存在负环
    queue<LL> q;
    q.push(start);
    vis[start] = 1;
    dist[start] = 0;
    while (!q.empty()) {
        LL np = q.front();
        q.pop();
        vis[np] = 0; // 出队则解除标记
        for (auto &x : pct[np])
            if (dist[x.first] > dist[np] + x.second) {
                dist[x.first] = dist[np] + x.second;
                dp[x.first] = dp[np] + 1; // 状态转移
                if (dp[x.first] > pct.size() - 2)
                    return 1;
                if (vis[x.first]) // 在队列中则不必反复入队
                    continue;
                vis[x.first] = 1;
                q.push(x.first);
            }
    }
    return 0;
}

void ac() {
    LL n, m;
    vvpll pct;
    cin >> n >> m;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
        if (w >= 0)
            pct[e].push_back({s, w});
    }
    if (spfa(pct, 1))
        cout << "YES\n";
    else
        cout << "NO\n";
}

int main() {
    LL T = 1;
    cin >> T;
    while (T--)
        ac();
    return 0;
}

单源最短路总结和OJ参考

常规dijkstra 堆优化dijkstra bellman-ford 算法 SPFA
算法思想 贪心: 每次拿出还未确定最短路的点中,距离起点最近的边; 打上标记之后,更新出边所连点的最短路。 使用堆优化找点操作 : 把还未确定最短路的点扔到堆中,用堆快速找出距离起点最近的边。 暴力松弛 : 执行 n-1 轮松弛操作; 每次都扫描所有的边,看看能否松弛 使用队列优化 bf 算法 : 只有上一轮被松弛的点,下一轮才有可能松弛。
负边权 失效 失效 可行 可行
负环 失效 失效 可以判断负环: 执行 n 轮操作,判断是否松弛 可以判断负环: 创建 cnt/dp 数组,标记从起点到该点的边数
时间复杂度 O(n^2) O(m log m) O(nm) O(km),最差情况 O(nm)

还有两个单源最短路径算法,那就是普通 bfs 以及 01bfs,但它们的使用情况有很大的局限性:

  • 普通 bfs 只能处理边权全部相同且非负的最短路;
  • 01bfs 只能解决边权要么为 0,要么为 1 的情况。

后续的 OJ 基本都是在 3 种最短路算法中进行微调,同时在问法上做很多的细节。

P1629 邮递员送信 - 洛谷

P1629 邮递员送信 - 洛谷

邮递员从结点 1 出发,每送完一个快递都要返回,去时走最短路,来时肯定也走最短路,所以存 2 个图,一个是从1出发去各结点,另一个是第 1 个图的反图(即有向图的每个边都相反但权值不变),两个图各自用最短路算法求一遍,求和即为答案。

数据量是 n = 10 3 n=10^3 n=103 , m = 10 5 m=10^5 m=105 ,这里的最短路算法都可以使用,这里使用堆优化的 dijistra 。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vb = vector<bool>;
using vvpll = vector<vector<pair<LL, LL>>>;
using pll = pair<LL, LL>;
using vpll = vector<pll>;

const LL INF = (1LL << 31) - 1;

LL dijistra(vvpll &pct, LL start) {
    vll dist(pct.size(), INF);
    vb vis(pct.size(), 0);
    priority_queue<pll, vpll, greater<pll>> pq;
    LL sum = 0;
    dist[start] = 0;
    pq.push({dist[1], 1});
    while (!pq.empty()) {
        pll np = pq.top();
        pq.pop();
        if (vis[np.second])
            continue;
        vis[np.second] = 1;
        for (auto &x : pct[np.second]) {
            if (dist[x.first] > dist[np.second] + x.second) {
                dist[x.first] = dist[np.second] + x.second;
                pq.push({dist[x.first], x.first});
            }
        }
    }
    for (int i = 1; i < dist.size(); i++)
        sum += dist[i];
    return sum;
}

int main() {
    LL n, m;
    vvpll pct, pct2;
    cin >> n >> m;
    pct.resize(n + 1);
    pct2.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        int s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, w});
        pct2[e].push_back({s, w}); // 建反图
    }
    cout << dijistra(pct, 1) + dijistra(pct2, 1);
    return 0;
}

P1744 采购特价商品 - 洛谷

P1744 采购特价商品 - 洛谷

题目问的是图上 2 个点的最短距离,除了边的权值都是浮点数,其余没什么特点,数据量小,这里的最短路算法都可以用。这里依旧选择使用堆优化的dijistra。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

struct solution { // 在类中可无视函数的先后顺序
    using pdd = pair<double, double>;
    using pid = pair<int, double>;
    using vvpid = vector<vector<pid>>;
    using pii = pair<int, int>;
    using vpii = vector<pii>;
    using pdi = pair<double, int>;

    int ac() {
        int n, m, start, end;
        vector<pii> pnt; // 坐标
        vvpid pct;       // 图
        cin >> n;
        pnt.resize(n + 1);
        pct.resize(n + 1);
        for (int i = 1; i <= n; i++) // 输入两点坐标
            cin >> pnt[i].first >> pnt[i].second;
        cin >> m;
        for (int i = 1; i <= m; i++) { // 存图
            int x, y;
            cin >> x >> y;
            double tmp = get_dist(pnt[x], pnt[y]);
            pct[x].push_back({y, tmp});
            pct[y].push_back({x, tmp});
        }
        cin >> start >> end; // 输入起点和终点
        printf("%.2lf", dijistra(pct, start, end));
        return 0;
    }

    double dijistra(vvpid &pct, int start, int end) {
        vector<double> dist(pct.size(), double(1u << 31) - 1);
        vector<bool> vis(pct.size(), 0);
        priority_queue<pdi, vector<pdi>, greater<pdi>> pq;
        dist[start] = 0;
        pq.push({0, start});
        while (pq.size()) {
            pdi np = pq.top();
            pq.pop();
            if (vis[np.second])
                continue;
            vis[np.second] = 1;
            for (auto &x : pct[np.second]) {
                if (dist[x.first] > dist[np.second] + x.second) {
                    dist[x.first] = dist[np.second] + x.second;
                    pq.push({dist[x.first], x.first});
                }
            }
        }
        return dist[end];
    }

    inline double get_dist(pii &a, pii &b) {
        double x1 = a.first, y1 = a.second, x2 = b.first, y2 = b.second;
        return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }
};

int main() {
    solution().ac();//生成匿名对象调用函数
    return 0;
}

P2136 拉近距离 - 洛谷

P2136 拉近距离 - 洛谷

事件为结点,效果是边权,可构建一个图模型。题目描述的事件转移后是减少 W i W_i Wi ,且求得是最短距离,或者说是最短路径长度,则每个边的权值要存为负数,才能使距离减少。

同时这题还玩了一个文字游戏:题目描述的这俩人很明显是距离表示好感度,若出现负环则感情无限,否则不仅要求男的通过一系列事件后和女的的感情,还要求女的和男的的感情,二者之中的最小值即为它们之间的感情距离,即 "爱情是双向奔赴" 。

判断负环 需要使用 ford 算法或 spfa 。这里使用 spfa ,分别求 1 为起点到 n n n 的最短路,和 n n n 为起点,到 1 的最短路,二者取最小值就是答案,前提是没有出现负环。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using pii = pair<int, int>;
using vvpii = vector<vector<pii>>;
using vi = vector<int>;
using vb = vector<bool>;

int spfa(vvpii &pct, int start, bool &fl) {
    vi dist(pct.size(), 1e9 + 7);
    vb vis(pct.size(), 0);
    vi dp(pct.size(), 0);
    queue<int> q;

    q.push(start);
    vis[start] = 1;
    dist[start] = 0;
    while (!q.empty()) {
        int np = q.front();
        q.pop();
        vis[np] = 0;
        for (auto &x : pct[np]) {
            if (dist[x.first] > dist[np] + x.second) {
                dist[x.first] = dist[np] + x.second;
                dp[x.first] = dp[np] + 1;
                if (dp[x.first] > pct.size() - 2) {
                    fl = 1; // 这里返回什么都无所谓,反正已经出现负环
                    return 0;
                }
                if (vis[x.first])
                    continue;
                vis[x.first] = 1;
                q.push(x.first);
            }
        }
    }
    // 返回值是细节,针对不同的起点需要返回特定的距离
    return dist[pct.size() - 1 - start + 1];
}

int main() {
    int n, m;
    vvpii pct;
    bool fg = 0;
    cin >> n >> m;
    pct.resize(n + 1);
    for (int i = 1; i <= m; i++) {
        int s, e, w;
        cin >> s >> e >> w;
        pct[s].push_back({e, -1 * w}); // 边权要存储相反数
    }
    bool fl = 0;
    int ans = spfa(pct, 1, fl);
    if (fl) {
        cout << "Forever love";
        return 0;
    }
    fl = 0;
    ans = min(ans, spfa(pct, n, fl));
    if (fl) {
        cout << "Forever love";
        return 0;
    }
    cout << ans;
    return 0;
}

P1144 最短路计数 - 洛谷

P1144 最短路计数 - 洛谷

这题是在无向无 (同) 权图的最短路的基础上求动态规划。

状态定义:dp[i] 表示起点 1 到 i 点的最短路的条数。

转移方程:

  • 在松弛操作用 j 更新 i 的最短路时,自己的方法不是最好的,于是别人的方法数就是自己的方法数:dp[i]=dp[j]
  • dist[i]==dist[j]+1 时,说明还有和目前的最短路方法一样的路径,于是叠加:dp[i]+=dp[j]
  • 若经过松弛操作后再判断 dist[i]==dist[j]+1 则会出错,所以松弛操作和相等是 2 个分支。

初始化:dp[1]=1 ,表示从自身到自身的最短路径数为 1 。其余可通过递推即可。

这题因为数据量,推荐使用堆优化 dijistra 或 SPFA 。这里用 SPFA 。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vvll = vector<vll>;
using vb = vector<bool>;

vll dp;

void spfa(vvll &pct) {
    vll dist(pct.size(), 1e9 + 7);
    vb vis(pct.size(), 0);
    dp.resize(pct.size(), 0);
    queue<LL> q;

    q.push(1);
    dist[1] = 0;
    dp[1] = 1;
    vis[1] = 1;
    while (!q.empty()) {
        LL np = q.front();
        q.pop();
        vis[np] = 0;
        for (auto &x : pct[np]) {
            if (dist[x] > dist[np] + 1) {
                dist[x] = dist[np] + 1;
                dp[x] = dp[np]; // 自己的方法不是最好的
                if (vis[x])
                    continue; // 防止重边重复入队造成干扰
                vis[x] = 1;
                q.push(x);
            } else if (dist[x] == dist[np] + 1) {
                dp[x] = (dp[np] + dp[x]) %
                        100003; // 还有和目前的最短路方法一样的路径
            }
        }
    }
}

int main() {
    LL n, m;
    vvll pct;
    cin >> n >> m;
    pct.resize(n + 1, vll(0));
    for (int i = 1; i <= m; i++) {
        int s, e;
        cin >> s >> e;
        pct[s].push_back(e); // 无向无权图
        pct[e].push_back(s);
    }
    spfa(pct);
    for (int i = 1; i < dp.size(); i++)
        cout << dp[i] << '\n';
    return 0;
}

若边权不全相等时,只能用 dijistra ,因为只有 dijistra 才能保证第一次求出的路径是最短路。

多源最短路

多源最短路,即图中每对顶点间的最短路径。

解决多源最短路最粗暴的思路是对每个点都进行一次单源最短路,即使是综合性能最优秀的堆优化 dijistra ,时间复杂度也要来到 O ( n m log ⁡ m ) \text{O}(nm\log m) O(nmlogm) ,一般的图论问题,边的数量往往特别庞大,有时甚至大于 n 2 n^2 n2 ,反而不如 O ( n 3 ) \text{O}(n^3) O(n3) 的 floyd 算法。

floyd算法

floyd 算法本质是动态规划,用来求任意两个结点之间的最短路,也称插点法,即通过不断在两点之间加入新的点,来更新最短路径。

即枚举任意 2 点,求它们的最短路,需要不断地引入其他结点。在求的时候不关心这些结点是如何遍历的,只关心最优的情况。

若仔细分析, floyd 和 01 背包的思想特别相似。

适用于任何图,不管有向无向,边权正负,但是最短路必须存在(也就是不存在负环)。floyd 也可以用于判断负环。

  1. 状态表示:
    dp[k][i][j] 表示:仅仅经过 [1, k] 这些点,结点 i i i 走到结点 j j j 的最短路径的长度。

  2. 状态转移方程:

    • 第一种情况,不选或者说不经过新来的点:
      dp[k][i][j] = dp[k-1][i][j]
    • 第二种情况,选择新来的点:
      dp[k][i][j] = dp[k-1][i][k] + dp[k-1][k][j]
      即先走到 k ,再 从 k 走到 j

    2 种情况取最小值即可。

  3. 空间优化:只会用到上一层的状态,因此可以优化到第一维。

  4. 初始化:

    • dp[i][i] = 0
    • dp[i][j] 为初始状态下 i i i 到 j j j 的距离,如果没有边则为无穷。

    dp[i][j] 其实就是一个邻接矩阵,因此可直接用邻接矩阵去递推,但要做好备份。

  5. 填表顺序:

  • 一定要先枚举 k k k,再枚举 i i i 和 j j j。因为我们填表的时候,需要依赖的是 k − 1 k-1 k−1 层的状态,因此 k k k 必须先枚举。

B3647 【模板】Floyd - 洛谷

B3647 【模板】Floyd - 洛谷

重边,无处不在,使用邻接矩阵很容易被坑。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vll = vector<LL>;
using vvll = vector<vll>;
using vb = vector<bool>;

const LL INF = 1e9 + 7;

void floyd(vvll &dp) {
    int n = dp.size() - 1;
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
}

int main() {
    vvll dp;
    int n, m;
    cin >> n >> m;
    dp.resize(n + 1, vll(n + 1, INF));
    for (int i = 1; i <= n; i++) // 直接在图上做dp
        dp[i][i] = 0;
    for (int i = 1; i <= m; i++) {
        LL s, e, w;
        cin >> s >> e >> w;
        dp[s][e] = dp[e][s] = min(dp[s][e], w); // 万恶的重边
    }
    floyd(dp);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++)
            cout << dp[i][j] << ' ';
        cout << '\n';
    }
    return 0;
}

P2910 Clear And Present Danger S - 洛谷

P2910 [USACO08OPEN\] Clear And Present Danger S - 洛谷](https://www.luogu.com.cn/problem/P2910) 岛看做结点,俩岛之间的危险系数看做边权,可将所有的危险系数和岛屿用一张无向图进行建模。 答案要的是指定航线内的危险系数和,相当于多个最短路之和。所以可用 floyd 算法求解多源最短路,然后根据航线对危险系数进行求和即可。 ```cpp #include using namespace std; using LL = long long; using vll = vector; using vvll = vector; using vb = vector; const LL INF = 1e9 + 7; void floyd(vvll &dp) { int n = dp.size() - 1; for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } int main() { vvll dp; vll line; LL n, m, sum = 0; cin >> n >> m; dp.resize(n + 1, vll(n + 1, INF)); line.resize(m + 1, 0); for (int i = 1; i <= m; i++) cin >> line[i]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) cin >> dp[i][j]; } floyd(dp); for (int i = 2; i < line.size(); i++) { sum += dp[line[i - 1]][line[i]]; } cout << sum; return 0; } ``` #### P1119 灾后重建 - 洛谷 理解floyd本质 [P1119 灾后重建 - 洛谷](https://www.luogu.com.cn/problem/P1119) 村庄看成结点,公路长看成边权,可建图模型。然后有若干次询问,每次都问具体时刻的两村庄之间的最短路,所以是多源最短路问题。 但题目引入一个设定,只有两个村庄修好,两个村庄才算是连通。且 Q Q Q 次询问保证 t t t 不下降。而 floyd 的最外层循环,**每次都是引入一个新结点作为任意两个结点之间的最短路求解的辅助**,就像 01 背包问题的第 1 层循环,每次都多引入 1 个物品去填满背包。所以可以在每次询问时逐层调用 floyd 算法更新 dp 表。 > 若是题目明确告诉每次询问都是乱序,则要建立多个 dp 表,表示第几天的状态。 其中对村庄修好时间的理解:例如 t 0 = 1 t_0=1 t0=1 ,则 0 号村庄在第 1 天就能恢复功能。 ```cpp #include using namespace std; using vi = vector; using vvi = vector; const int INF = 1e9 + 7; void floyd(vvi &dp, int k) { // 加点辅助求最短路 for (int i = 0; i < dp.size(); i++) for (int j = 0; j < dp.size(); j++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } int main() { int n, m, T; vi times; vvi pct; cin >> n >> m; times.resize(n, 0); for (int i = 0; i < n; i++) cin >> times[i]; pct.resize(n, vi(n, INF)); for (int i = 0; i < n; i++) pct[i][i] = 0; for (int i = 0; i < m; i++) { int s, e, w; cin >> s >> e >> w; pct[s][e] = pct[e][s] = min(pct[s][e], w); } cin >> T; int pos = 0; while (T--) { int x, y, t; cin >> x >> y >> t; while (pos < n && t >= times[pos]) { // pos每增加1,则多一个村子修复 floyd(pct, pos++); } if (times[x] > t || times[y] > t || pct[x][y] == INF) // 实在来不及修复或没路的情况 cout << -1 << '\n'; else cout << pct[x][y] << '\n'; } return 0; } ``` #### P6175 无向图的最小环问题 - 洛谷 [P6175 无向图的最小环问题 - 洛谷](https://www.luogu.com.cn/problem/P6175) 寻找无向图的最小环,首先就是保证不重复地分类。可按环的最大编号进行分类。如下图所示: ![请添加图片描述](https://i-blog.csdnimg.cn/direct/9c8eb4a804974875a17b8b4d2a877cde.png) 对这些环求 `min` 就是本题的答案。 floyd 算法是在已经引入 `[1,k-1]` 这些结点求得的 `i` 到 `j` 的最短路的基础上,再引入结点 `k` 试图求解更优的最短路。这里 ( i , j , k ) (i,j,k) (i,j,k) 本身就可以组成一个环。 但环 ( i , j , k ) (i,j,k) (i,j,k) 不是全都符合要求。 * 例如若 i = j i=j i=j 或 j = k j=k j=k 时,尽管也有可能组成环(重边的情况),但因为有一边的权值变成了零,使得环反而变小,很可能得出错误的答案,更何况这题要求环至少拥有 3 个结点。 * 例如使用递推表里的 `dp[i][j]+dp[i][k]+dp[k][j]` 求环时,因为递推表里引入其他结点,这些结点的编号可能很小,导致结果反而特别小。 所以符合要求的环 ( i , j , k ) (i,j,k) (i,j,k) 需满足 i ≠ j , i ≠ k , j ≠ k i\\neq j,i\\neq k,j\\neq k i=j,i=k,j=k ,且 `dp[i][j]` 需要在引入 `[1,k]` 更新最短距离的基础上,加上原图的边权之和 `pct[i][k]+pct[k][j]` ,这样的环才符合要求。 枚举这样的环,只需在 floyd 算法的基础上进行修改即可。 例如不在意 i , j i,j i,j 的大小,只需满足 i ≠ j , i ≠ k , j ≠ k i\\neq j,i\\neq k,j\\neq k i=j,i=k,j=k 即可,因为当前最外层为第 k k k 层循环,即使 i , j i,j i,j 均大于 k k k ,但 `dp[i][j]` 也是通过 `[1,k-1]` 求得最短路的结果,也是符合要求的环。 ```cpp using LL = long long; using vll = vector; using vvll = vector; void floyd(vvll &dp, vvll &pct, LL &ans) { int n = dp.size() - 1; for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { // 求最小环,其中dp[i][j]为引入[1,k-1]后,i到j的最小值 if (i != j && j != k && i != k) ans = min(ans, dp[i][j] + pct[i][k] + pct[k][j]); // 求最短路 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); //到这里时dp[i][j]的性质就变了,不能再用于求解最小环 } } } ``` 也可以先遍历所有符合条件的环,再求最短路。 ```cpp using LL = long long; using vll = vector; using vvll = vector; void floyd(vvll &dp, vvll &pct, LL &ans) { int n = dp.size() - 1; for (int k = 1; k <= n; k++) { // 求最小环,其中dp[i][j]为引入[1,k-1]后,i到j的最小值 for (int i = 1; i < k; i++) for (int j = i + 1; j < k; j++) ans = min(ans, dp[i][j] + pct[i][k] + pct[k][j]); //求最短路 for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } } ``` 两种思路都可以。这里选第 1 种。 ```cpp #include using namespace std; using LL = long long; using vll = vector; using vvll = vector; const LL INF = 1e9 + 7; void floyd(vvll &dp, vvll &pct, LL &ans) { int n = dp.size() - 1; for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { // 求最小环,其中dp[i][j]为引入[1,k-1]后,i到j的最小值 if (i != j && j != k && i != k) ans = min(ans, dp[i][j] + pct[i][k] + pct[k][j]); // 求最短路 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } } } int main() { LL n, m, ans = INF; vvll pct, pct2; cin >> n >> m; pct.resize(n + 1, vll(n + 1, INF)); for (int i = 0; i <= n; i++) pct[i][i] = 0; for (int i = 1; i <= m; i++) { LL s, e, w; cin >> s >> e >> w; pct[s][e] = pct[e][s] = min(pct[s][e], w); } pct2 = pct; // 拷贝构造函数 floyd(pct, pct2, ans); if (ans == INF) cout << "No solution."; else cout << ans; return 0; } ``` ## OJ参考 1. 单源最短路 [P3371 【模板】单源最短路径(弱化版) - 洛谷](https://www.luogu.com.cn/problem/P3371) [P4779 【模板】单源最短路径(标准版) - 洛谷](https://www.luogu.com.cn/problem/P4779) [P3385 【模板】负环 - 洛谷](https://www.luogu.com.cn/problem/P3385) [P1629 邮递员送信 - 洛谷](https://www.luogu.com.cn/problem/P1629) [P1744 采购特价商品 - 洛谷](https://www.luogu.com.cn/problem/P1744) [P2136 拉近距离 - 洛谷](https://www.luogu.com.cn/problem/P2136) [P1144 最短路计数 - 洛谷](https://www.luogu.com.cn/problem/P1144) 2. 多源最短路 [B3647 【模板】Floyd - 洛谷](https://www.luogu.com.cn/problem/B3647) \[P2910 [USACO08OPEN\] Clear And Present Danger S - 洛谷](https://www.luogu.com.cn/problem/P2910) [P1119 灾后重建 - 洛谷](https://www.luogu.com.cn/problem/P1119) [P6175 无向图的最小环问题 - 洛谷](https://www.luogu.com.cn/problem/P6175)

相关推荐
Filotimo_2 小时前
3.4 图
算法·图论
I_LPL2 小时前
day49 代码随想录算法训练营 图论专题2
java·算法·深度优先·图论·广度优先·求职面试
小小unicorn2 小时前
[微服务即时通讯系统]语音子服务的实现与测试
c++·算法·微服务·云原生·架构·xcode
xsyaaaan2 小时前
代码随想录Day53图:Floyd算法精讲_ Astar算法精讲_最短路算法总结篇_图论总结
算法·图论
lihihi2 小时前
P10471 最大异或对 The XOR Largest Pair
算法
小白学大数据2 小时前
Pycharm 断点调试 Scrapy:两种实现方式总结
c++·爬虫·scrapy·pycharm
林鸿群2 小时前
VS2026 + C++ 游戏服务器集群编译部署实战(14 个组件完整流程)
服务器·c++·游戏·mfc·游戏服务器·vs2026·编译部署
漫随流水2 小时前
备战蓝桥杯(3)
数据结构·c++·算法·蓝桥杯
song8546011342 小时前
hash和history导航区别 个别服务器为啥不支持 history 模式
服务器·算法·哈希算法