文章目录
简介
单源最短路:求所有点到某源点的最短路径。
多源最短路:求所有点到某些/所有源点的最短路径。
最短路算法的分类:
单源最短路
所有边权都是正数
朴素的Dijkstra算法 O(v^2) 适合稠密图
堆优化版的Dijkstra算法 O(elogv)适合稀疏图,set或手写堆:O(elogv),优先队列:O(eloge),因为差距在log内,所以可以忽略不计
存在负权边
Bellman-Ford O(ve) 可用于判断是否有负环:v次循环后,再进行一次循环仍有状态更新
spfa 一般O(e),最坏O(ve)
多源最短路
Floyd算法 O(v^3)
稠密图用邻接矩阵存,稀疏图用邻接表存储。
稠密图: m 和 n² 一个数量级
稀疏图: m 和 n 一个数量级
1.Dijkstra算法
思路
Dijkstra算法采用的是一种贪心的策略,声明一个数组dist来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dist[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dist[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dist数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点c并且看看通过该顶点到达其他点的路径长度是否比dist[c]小,如果是,那么就替换这些顶点在dist中的值。
然后,又从dist中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
伪代码
集合S:当前已经确定最短距离的点
dist[1] = 0, dist[i] = 正无穷 (i不等于1)
for v: 1 ~ n
找出不在S中的距离最近的点t(S中最小dist[t])
把t放到S中
用t更新其他点的距离(对t的所有边进行松弛操作)
1.1 朴素的Dijkstra算法
使用场景
适合于稠密图,时间复杂度O(n²),用邻接矩阵(或邻接表)来存储。
算法模板
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int K) {
const int INF = 0x3f3f3f3f;
vector<vector<int>> g(n+1, vector<int>(n+1, INF));
for (auto &v: times){//建立邻接矩阵
g[v[0]][v[1]] = v[2];
}
vector<int> dist(n+1, INF); // 距离起始点的最短距离,节点1~n
vector<bool> st(n+1, false); // 是否已经得到最优解
dist[K] = 0; // 起始点
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])){
t = j;
}
}
st[t] = true; // t号点的最短路已经确定
for (int j = 1; j<=n; j++){ // 用t更新其他点的距离
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
1.2 堆优化版的Dijkstra算法
使用场景
适合于稀疏图,用邻接表来存储数据,调用set或手写堆时间复杂度O(elogv),调用优先队列时间复杂度O(eloge)。
模板
// first表示另一个端点, second表示距离/权重
using PII = pair<int, int>;
// dist[i]表示点i到k的最短路径
vector<int> dist;
vector<vector<PII>> graph; // 邻接表;first表示另一个端点, second表示距离/权重
// n个顶点,edges[0]、edges[1]、edges[2]分别表示点u、v、两点距离
void Dijkstra(int n, vector<vector<int>>& edges, int k) {
const int inf = 0x3f3f3f3f;
dist = vector<int>(n + 1, inf);
dist[k] = 0;
vector<bool> exist(n + 1, 0); // 最短距离是否已确定
graph = vector<vector<PII>>(n + 1);
for (const auto& vec : edges) {
graph[vec[0]].push_back({ vec[1], vec[2] });
graph[vec[1]].push_back({ vec[0], vec[2] });
}
priority_queue<PII, vector<PII>, greater<>> pq;
pq.push({ 0, k });
while (pq.size()) {
auto [dis, v] = pq.top();
pq.pop();
if (exist[v])
continue; // v已经作为顶点,松驰过临边
exist[v] = true;
for (auto [u, d] : graph[v]) {
if (d + dis < dist[u]) {
dist[u] = d + dis;
pq.push({ dist[u], u });
}
}
}
}
【注】因为每个边都可能添加到堆中,所以严格来说是O(eloge)时间复杂度。如果想要严格O(elogn),可以使用红黑树set实现,通过删除set中旧节点,来保证不会有重复节点。
2.Bellman-Ford算法
2.1 算法思想
【注】上述公式适用于无边数限制,如果有边数K限制,则循环K次,上述松弛操作修改为:dis[v]=min(dist[v],pre[u]+w);//pre为上一次循环的结果
【证明】
设dp(k, v)表示最多经过k个节点中转由src到v的最低花费;
则dp(k, v) = min{dp(k-1, v), dp(k-1, u) + cost(u, v), dp(k-1,w)+ cost(u,w),…}
(u,w,…为v的所有邻接节点)
2.2 应用
2.3 代码
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
const int INF=0x3f3f3f3f;
vector<int> dis(n,INF);
dis[src]=0;
for(int i=0;i<=K;i++){
vector<int> pre=dis;
for(auto flight:flights){
dis[flight[1]]=min(dis[flight[1]],pre[flight[0]]+flight[2]);
}
}
return dis[dst]==INF?-1:dis[dst];
}
};
时间复杂度O(ve)。
3.spfa算法
3.1 算法思路
3.2 算法步骤
3.3 使用场景
3.4 spfa算法求负环
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
int n, m; // n个点,m条边
const int N = 2010;
int dist[N]; // 到起始点的最小距离
bool st[N]; // 在队列中是否存在
int cnt[N]; // 记录最短路的边数
bool spfa(){
queue<int> q;
for (int i = 1; i <=n; i++){ // 所有点入队列;负环可能存在在所有点出发的最短路上
q.push(i);
st[i] = true;
}
while (!q.empty()){
int u = q.front();
q.pop();
st[u] = false; // 不在队列
for (auto &e: edges[u]){
int v = e.first, w = e.second;
if (dist[v] > dist[u] + w){
dist[v] = dist[u] + w; // 更新最短路 权值
cnt[v] = cnt[u] + 1; // 更新经过的边数
// 存在负环;边数>=n,经过的点>=n+1;根据抽屉原理得,最短路存在负环
if (cnt[v] >= n) return true;
if (!st[v]){
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main(){
cin >> n >> m;
while (m--){ // 构造图
int u, v, w;
cin >> u>> v>> w;
edges[u].push_back({v, w});
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
4.Floyd算法
4.1 代码
求多源汇最短路径
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][k] + d[k][j]);
4.2 算法原理
floyd算法是基于动态规划的
5. 例题
【方法一】朴素Dijkstra
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int K) {
const int INF = 0x3f3f3f3f;
vector<vector<int>> g(n+1, vector<int>(n+1, INF));
for (auto &v: times){//建立邻接矩阵
g[v[0]][v[1]] = v[2];
}
vector<int> dist(n+1, INF); // 距离起始点的最短距离
vector<bool> st(n+1, false); // 是否已经得到最优解
dist[K] = 0; // 起始点
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])){
t = j;
}
}
st[t] = true; // t号点的最短路已经确定
for (int j = 1; j<=n; j++){ // 用t更新其他点的距离
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
【方法二】堆优化的Dijkstra
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
typedef pair<int, int> PII; // first:距离; second: 几号点
vector<int> dist(N+1, INF); // 距离起始点的最短距离
vector<vector<PII>> graph(n+1); // 邻接表;u->v,权重w
set<PII,greater<PII>> iset;
for (auto &t: times){ // 初始化邻接表
graph[t[0]].push_back({t[2],t[1]});
}
iset.insert({0,K});
dist[K] = 0;
while(iset.size()){
PII t= *iset.begin();
iset.erase(iset.begin());
int ver=t.second, distance= t.first;
for (auto &p: graph[ver]){
if (dist[p.second] > distance + p.first){ // 用t去更新其他点到起始点的最短距离
iset.erase({dist[p.second],p.second});
dist[p.second] = distance + p.first;
iset.insert({dist[p.second], p.second});
}
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
【方法三】Bellman-Ford算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF=0x3f3f3f3f;
vector<int> distoK(N+1,INF);
distoK[K]=0;
for(int i=0;i<N;i++){
vector<int> pre=distoK;
for(vector<int> &time:times){
distoK[time[1]]=min(distoK[time[1]],pre[time[0]]+time[2]);
}
}
int res=*max_element(distoK.begin()+1,distoK.end());
return res==INF?-1:res;
}
};
【方法四】SPFA
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
typedef pair<int, int> PII; // first:距离; second: 几号点
vector<int> dist(N+1, INF); // 距离起始点的最短距离
dist[K]=0;//初始点,容易忘
vector<vector<PII>> graph(N+1); // 邻接表;u->v,权重w
for (auto &t: times){ // 初始化邻接表
graph[t[0]].push_back({t[2],t[1]});
}
queue<int> modifyque;
modifyque.push(K);
vector<bool> st(N+1, false); // 是否在队列中,避免重复进队列
st[K] = true;
while(!modifyque.empty()){
int tmp=modifyque.front();
modifyque.pop();
st[tmp]=false;
for(PII &p:graph[tmp]){
int cost=p.first;
int dest=p.second;
if(dist[tmp]+cost<dist[dest]){
dist[dest]=dist[tmp]+cost;
if(!st[dest]){
modifyque.push(dest);
st[dest]=true;
}
}
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
【方法五】floyd算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
vector<vector<int>> d(N+1, vector<int>(N+1, INF));
for (int i = 1; i<=N; i++) d[i][i] = 0;
for (auto &t: times){
d[t[0]][t[1]] = min(d[t[0]][t[1]], t[2]);
}
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][k] + d[k][j]);
}
}
}
int ans = 0;
for (int i =1; i<=N; i++){
ans = max(ans, d[K][i]);
}
return ans > INF/2 ? -1: ans;
}
};