图论------最短路问题
- 单源最短路
-
- 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点是否已经确定了最短路径。
- 创建一个长度为 n n n 的
-
初始化:
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 【模板】单源最短路径(弱化版) - 洛谷
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点是否已经确定了最短路径; - 创建一个小根堆,维护更新后的结点。(也就是需要确定最短路径的结点)
- 创建一个长度为 n n n 的
-
初始化:
dist[1] = 0,然后将{0, s}加到堆里,其余结点的dist值为无穷大,表示还没有找到最短路径。 -
重复:弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有标记过,打上标记,进行松驰操作。
-
重复上述操作,直到堆里面没有元素为止。
如果出现负权边,就会出错,无论如何优化都是如此。例如这个图:

这里 1 到 3 的最短路径长是 -3 ,使用 dijistra 算法就会出错。
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) ,因为堆维护的是边,遍历邻接表时也是遍历边,其他的还有 dist 、 vis 的初始化和点有关,相对与 m m m 比较小,可近似忽略。
bellman-ford 算法
Bellman-Ford 算法 ( 简称 BF 算法 ) 是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路 ,并可以对最短路不存在的情况进行判断。
最短路不存在的情况:负环中的负边权绝对值过大,使得最短路可以无限减小。
算法核心思想:不断尝试对图上每一条边进行松弛,直到所有的点都无法松弛为止。因此可以选择不存图,而是存边。
Bellman-Ford 算法流程:
-
准备工作:
- 创建一个长度为 n n n 的
dist数组,其中dist[i]表示从起点到 i i i 结点的最短路径。
- 创建一个长度为 n n n 的
-
初始化 :
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 点是否已经在队列中。
- 创建一个长度为 n n n 的
-
初始化 :标记
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 【模板】负环 - 洛谷
负环使 dijistra 这位伟大的天骄夭折于此,但却给 ford 算法和 spfa 开了一扇门。
例如测试样例给的负环:

这个负环不存在单源最短路。
若使用 ford 运行这个图,执行完 n − 1 n-1 n−1 次松弛操作时,第 n n n 次必定不会进行,除非它是负环,使得松弛操作可以永远进行下去。
ford 算法判断负环
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 判断负环
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]+1 ,dp[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 邮递员送信 - 洛谷
邮递员从结点 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 采购特价商品 - 洛谷
题目问的是图上 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 拉近距离 - 洛谷
事件为结点,效果是边权,可构建一个图模型。题目描述的事件转移后是减少 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 最短路计数 - 洛谷
这题是在无向无 (同) 权图的最短路的基础上求动态规划。
状态定义: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 也可以用于判断负环。
-
状态表示:
dp[k][i][j]表示:仅仅经过[1, k]这些点,结点 i i i 走到结点 j j j 的最短路径的长度。 -
状态转移方程:
- 第一种情况,不选或者说不经过新来的点:
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 种情况取最小值即可。
- 第一种情况,不选或者说不经过新来的点:
-
空间优化:只会用到上一层的状态,因此可以优化到第一维。
-
初始化:
dp[i][i] = 0dp[i][j]为初始状态下 i i i 到 j j j 的距离,如果没有边则为无穷。
dp[i][j]其实就是一个邻接矩阵,因此可直接用邻接矩阵去递推,但要做好备份。 -
填表顺序:
- 一定要先枚举 k k k,再枚举 i i i 和 j j j。因为我们填表的时候,需要依赖的是 k − 1 k-1 k−1 层的状态,因此 k k k 必须先枚举。
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