前言
最短路问题是图论中很重要的问题,还有一个很重要的是拓扑排序。
最短路问题有很多应用,研究这个问题有相当的价值。
这里以题列举方法。选取了
我的题解不以结构体等格式表示点,看了很多人的题解根本看不下去,其命名完全没有含义,由单个字母组成,不具备可读性。
在阅读这篇文章前,请确保你已经掌握对应的方法。如果没有,请到最下面的参考中学习,那是我看过的写的相对较好的几篇文章。
符号约定
graph :
或是邻接表或是邻接矩阵dist :
最短距离数组used :
已走过数组mmax :
最终结束距离最大值,若为 INF 则说明没走到i, j :
只是下标E :
edge 表示边数 在分析性能时写成MV :
vertex 表示点数 在分析性能时写成N
题目1:
有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
本题数据最大100,故选取 1e4 为 INF
方法1 朴素dijkstra
采用邻接矩阵存储
其中 mmin 的使用需要注意
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<int>> graph(n, vector<int>(n, 1e4));
for (auto &x : times) {
graph[x[0] - 1][x[1] - 1] = x[2];
}
vector<int> dist(n, 1e4);
vector<bool> used(n);
int i, j;
dist[k - 1] = 0;
while (true) {
int mmin = -1;
for (i = 0; i < n; ++i) {
if (!used[i] && (mmin == -1 || dist[mmin] > dist[i]))
mmin = i;
}
if (mmin == -1)
break;
used[mmin] = true;
for (j = 0; j < n; ++j) {
if(!used[j])
dist[j] = min(dist[j], dist[mmin] + graph[mmin][j]);
}
}
int mmax = *max_element(dist.begin(), dist.end());
return mmax >= 1e4 ? -1 : mmax;
}
性能
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
空间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
方法2 堆优化dijkstra
用邻接表实现,普通的邻接表就不写了,跟上面差不多。
堆优化的好处在于每次选取最小值都是
O
(
1
)
O(1)
O(1),插入、删除是
O
l
o
g
n
O{logn}
Ologn
需要用 pair 记录出结点和距离
其中优先队列的重载符号方法比较特别,需要注意,我们并不能直接在括号里使用 lambda 重载。
我们让其以距离大小作为对比,STL默认大根堆,所以比较大小请反过来。
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int, int>>> graph(n);
int i;
for (i = 0; i < times.size(); ++i) {
auto& temp = times[i];
graph[temp[0] - 1].emplace_back(make_pair(temp[1] - 1, temp[2]));
}
auto cmp = [](const pair<int, int>& a, const pair<int, int>& b){
return a.second > b.second;
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)>
minHeap(cmp);
vector<int> dist(n, INT_MAX >> 2);
vector<bool> used(n);
minHeap.emplace(k - 1, 0);
dist[k - 1] = 0;
while (!minHeap.empty()) {
int t = minHeap.top().first;
int time = minHeap.top().second;
minHeap.pop();
if (used[t]) continue;
used[t] = true;
for (auto & x : graph[t]) {
if (!used[x.first] && dist[t] + x.second < dist[x.first]) {
dist[x.first] = dist[t] + x.second;
minHeap.emplace(x.first, dist[x.first]);
}
}
}
int mmax = *max_element(dist.begin(), dist.end());
if (mmax == INT_MAX >> 2)
return -1;
return mmax;
}
};
性能
这里的时间复杂度仅是一个上界,我们考虑每一次遍历都会遍历所有边,所以至少有一个M
那堆的大小是多大呢?显然上界是N ,理由是一个已经进入过堆的结点不会再次入堆 故 2logn
时间复杂度:
O
(
M
l
o
g
N
)
O(MlogN)
O(MlogN)
空间复杂度:
O
(
N
+
M
)
O(N + M)
O(N+M)
这里足可以看出,对于稠密图,即 M>>N 堆优化的方法可能比上面的方法慢一点
方法3 bellman-ford
此方法可以解决负权问题,上面的方法不可以,因为 dijkstra 基于贪心,基于眼下的判断一定会先选择负的,看不到未来。
明天补
- bellman-ford 2023/05/07
对这个方法的理解在于每次让每个距离已知的点沿它的边走一步(如果这样走更短的话),最后的结果相当于从源点走了 n - 1 步,所以可以看到在下面的 i 循环中,n - 1 是最紧的,n也可以。
但如果路上有负环,则即使过了 n - 1 次,也还会更新。理由是其会反复走负边因为负边永远更短。这个特性允许我们检测负环
需要注意的是需要一个clone 数组保持上一轮的状态,否则很有可能这一轮更新的距离被这一轮的点拿来使用,即沿某个点走了两步。
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<int>> graph(n, vector<int>(n, 1e4));
for (auto &x: times) {
graph[x[0] - 1][x[1] - 1] = x[2];
}
vector<int> dist(n, 1e4);
vector<int> clone(n);
dist[k - 1] = 0;
int i, j, t;
for (i = 0; i < n - 1; ++i) {
clone = dist;
for (j = 0; j < n; ++j) {
for (t = 0; t < n; ++t) {
dist[t] = min(dist[t], clone[j] + graph[j][t]);
}
}
}
int mmax = *max_element(dist.begin(), dist.end());
return mmax >= 1e4 ? -1 : mmax;
}
};
性能
时间复杂度:
O
(
N
3
)
O(N^3)
O(N3)
空间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
这里的used数组意思为在不在队列里。SPFA必须使用邻接表,否则多出的会破坏其时间复杂度。
方法4 SPFA
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int, int>>> graph(n);
for (auto &x: times) {
graph[x[0] - 1].emplace_back(x[1] - 1, x[2]);
}
vector<int> dist(n, 1e4);
// used to show is it in the queue
vector<bool> used(n);
int i;
queue<int> order;
order.push(k - 1);
dist[k - 1] = 0;
used[k - 1] = true;
while (!order.empty()) {
int temp = order.front();
order.pop();
used[temp] = false;
for (auto &[to, time]: graph[temp]) {
if (dist[to] > dist[temp] + time) {
dist[to] = dist[temp] + time;
if (!used[to]) {
used[to] = true;
order.push(to);
}
}
}
}
int mmax = *max_element(dist.begin(), dist.end());
return mmax >= 1e4 ? -1 : mmax;
}
};
性能
时间复杂度:
O
(
K
M
)
O(KM)
O(KM)
空间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
其中,K是节点被平均入队的次数。一般情况下是2,但最坏情况是
O
(
N
M
)
O(NM)
O(NM)
所以为了避免最坏情况,在全是正权边时使用 dijkstra
题目1总结
本题数据
- 1 <= k <= n <= 100
- 1 <= times.length <= 6000
M >> N,稠密图。
所以方法一的做法比方法二快些
方法一、二、四均大概90ms,方法三160ms
题目二:
有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。
本题数据最大10000,故选取 1e5 为 INF
这里我们看出需要控制步数,那么 bellman-ford 和 spfa将是可行的方法
方法1 Bellman-ford
直接基于边走,不用建图
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector dist(n, 1e5);
vector clone(n);
int i, j, f = flights.size();
dist[src] = 0;
for (i = 0; i <= k; ++i) {
clone = dist;
for (j = 0; j < f; ++j) {
int from = flights[j][0];
int to = flights[j][1];
int dist_ = flights[j][2];
if (dist[to] > clone[from] + dist_) {
dist[to] = clone[from] + dist_;
}
}
}
return dist[dst] >= 1e5 ? -1 : dist[dst];
}
};
性能
时间复杂度:
O
(
k
M
+
N
)
O(kM + N)
O(kM+N)
空间复杂度:
O
(
N
)
O(N)
O(N)
对于这个方法,使用 java 跑只需4ms,用c++需要32ms。非常奇怪,原因我觉得是
java 的 clone 或许自带优化?
方法二 SPFA
这里亦需要 clone 数组,用来限制步数,不用便会错。
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector<vector<pair<int, int>>> graph(n);
for (auto &x : flights) {
graph[x[0]].emplace_back(x[1], x[2]);
}
vector<int> dist(n, 1e5);
vector<int> clone(n);
vector<bool> used(n);
queue<int> order;
order.push(src);
used[src] = true;
dist[src] = 0;
int i, j;
for (i = 0; i <= k; ++i) {
int t = order.size();
clone = dist;
for (j = 0; j < t; ++j) {
int temp = order.front();
order.pop();
used[temp] = false;
for (auto &[to, dist_] : graph[temp]) {
if (dist[to] > dist_ + clone[temp]) {
used[to] = true;
dist[to] = dist_ + clone[temp];
order.push(to);
}
}
}
}
return dist[dst] >= 1e5 ? -1 : dist[dst];
}
};
性能
时间复杂度:
O
(
k
N
2
)
O(kN^2)
O(kN2)
空间复杂度:
O
(
N
)
O(N)
O(N)
其中,最坏情况为队列中有所有的点,而每个点往外的边数上界是 N - 1
但实际情况一般不会这么差。
题目2总结
本题数据
- 1 <= n <= 100
- 0 <= flights.length <= (n * (n - 1) / 2)
稠密图。
这题 bellman-ford 不建图效率挺高
SPFA最快,大概12ms