单源最短路径 Dijkstra算法
Dijkstra算法的策略是:假设顶点集为V,设置集合S存放已被访问的顶点,然后执行下面两个步骤n次:(n为顶点数目)
每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。
之后,令顶点u作为中介点,优化起点s与所有从u能到达且未访问过的顶点v的最短距离。
实现策略
集合S可用一个bool型数组vis[]来实现,vis[i]==true表示顶点Vi已经被访问;int型数组d[]表示起点s到顶点Vi的最短距离,初始时d[s]赋值为0,其余顶点赋值一个很大的数,来表示不可达。
伪代码
// G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度,s为起点 Dijkstra(G,d[],s) { for(循环n次) { u = 使d[u]最小的还未被访问的顶点的标号; 记u已被访问; for(从u出发能到达的所有顶点v) { if(v未被访问&&以u为中介点能使s到顶点v的最短距离d[v]更优){ 优化d[v]; } } } }
时间复杂度
外层循环控制必须把每个结点都标记为访问,O(n),内层循环需要枚举寻找最小的d[u],也为O(n) 故时间复杂度O(n^2)注意:dijkstra算法只能应对所有边权都是非负数的情况,出现负边权最好使用SPFA算法。
Bellman-Ford(BF)算法
BF算法可解决单源最短路径问题,也能处理带负权边的情况。当图中存在环时,根据环中边权之和的正负,可以将环分为零环、正环和负环。显然,零环和正环不会影响最短路径的求解,同时有源点不可达的负环存在也不影响最短路径求解;当有源点可达的负环时,最短路径不存在。
Bellman-Ford算法同样设置数组d[]存放源点到各顶点的最短距离,且BF算法返回一个bool值,如果有源点可达的负环则返回false;否则返回true,此时数组d中的值就是源点到各顶点的最短距离。
算法主要思路:对图中的边进行V-1轮的操作,每轮都遍历图中的所有边:对每条边u->v,如果以u为中介点可以使d[v]更小,则用d[u]+weight(u,v)更新d[v]。时间复杂度:O(VE),V顶点个数,E是边数。
伪代码
for(int i=0;i<n-1;i++) { for(对每条边 u->v){ if(d[u]+weight(u,v)<d[v]) d[v] = d[u] + weight(u,v); } } // 检查是否存在源点可达的负环 // 只需要再对所有边进行一次遍历,看是否能继续松弛 for(每条边u->v){ if(d[u]+weight(u,v)<d[v]){ return false; } } return true;
SPFA
- BF算法思路简洁但复杂度确实高。我们发现,只有当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的值d[v]才有可能改变。因此可以进行如下优化:建立队列,每次将队首u取出,如果d[v]>d[u]+weight(u,v)成立,即可以松弛操作,则更新d[v],将顶点v加入队列中。这样操作直到队列为空。如此优化后的算法称为SPFA。
- 复杂度O(kE),k为常数,E为边数。(使用数组num记录每个顶点入队的次数,如果顶点i的num[i]>=N,则说明存在源点可达的负环。)
Floyd算法 全源最短路问题
Floyd算法基于这样一个事实:如果存在顶点k,使得以k作为中介点时顶点i和顶点j的当前最短距离缩短,那么更新d[i][j] = d[i][k] + d[k][j](其中d[i][j]表示顶点i到顶点j的最短距离。
伪代码
枚举顶点k∈[1,n] 以顶点k作为中介点,枚举所有的顶点对i和j (i∈[1,n],j∈[1,n]) 如果d[i][j]>d[i][k]+d[k][j] 赋值d[i][j] = d[i][k] + d[k][j]
- 可以看到,Floyd算法的思想异常简洁。时间复杂度O(n^3)
例1
朴素dijkstra O(n^2)
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { vector<vector<long long>>graph(N+1,vector<long long>(N+1,INT_MAX)); // 构建图 for(int i=1;i<=N;++i) graph[i][i] = 0; for(auto it:times) { graph[it[0]][it[1]] = it[2]; } // 记录各顶点是否访问过 vector<bool>visit(N+1,false); // 源点到各顶点的距离 vector<long long>d(N+1,INT_MAX); // 初始化 d[K] = 0; // 循环N次 for(int i=1;i<=N;++i) { int u = -1; long long min_dis = INT_MAX; // 找到未访问顶点中离源点最近的 for(int j=1;j<=N;++j) { /////// 第一次必定选中从源点s出发 if(!visit[j] && d[j]<min_dis) { min_dis = d[j]; u = j; } } if(u==-1) break; visit[u] = true; for(int k=1;k<=N;k++) { // 以u作为中介点可以到达的所有点,如果以u为中介可以使d[k]更优,则更新 if((graph[u][k]!=INT_MAX) && (!visit[k]) && (d[k]>d[u]+graph[u][k])) d[k] = d[u] + graph[u][k]; } } int ans = INT_MIN; for(int i=1;i<=N;++i) { if(d[i]==INT_MAX) return -1; ans = max(ans,(int)d[i]); } return ans; } };
堆优化 dijkstra O(E*log(V))
class Solution { public: struct cmp{ bool operator()(pair<int,int>& a,pair<int,int>& b) { return a.first > b.first; // 小顶堆 } }; int networkDelayTime(vector<vector<int>>& times, int N, int K) { // first是距离,second是目标点 priority_queue<pair<int,int>,vector<pair<int,int>>,cmp>pq; // 源点到各顶点的距离 vector<int>d(N+1,INT_MAX); d[K] = 0; // 起点入队 pq.push(make_pair(0,K)); while(!pq.empty()) { // it为连接已访问点和未访问点的最小边 auto it = pq.top(); pq.pop(); // 如果这条边的长度已经比最短路径还大,则不可能缩短路径 // 说明这个pair保持的数据过时,最短路径值已经更新过 if(it.first>d[it.second]) continue; for(int i=0;i<times.size();++i) { if(times[i][0]==it.second) { int v = times[i][1]; int w = times[i][2] + it.first; if(d[v]>w) { pq.push(make_pair(w,v)); d[v] = w; } } } } int ans = INT_MIN; for(int i=1;i<=N;++i) { if(d[i]==INT_MAX) return -1; ans = max(ans,d[i]); } return ans; } };
BF算法
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { vector<int>d(N+1,INT_MAX); d[K] = 0; // Bellman-Ford // 执行N-1轮 for(int i=1;i<N;++i) { // 每一轮遍历所有边 for(auto it:times) { int u = it[0]; int v = it[1]; int w = it[2]; // 松弛 if(d[u]!=INT_MAX && d[u]+w<d[v]) d[v] = d[u] + w; } } int ans = INT_MIN; for(int i=1;i<=N;++i) { if(d[i]==INT_MAX) return -1; ans = max(ans,d[i]); } return ans; } };
SPFA
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { // 建图 vector<vector<int>>graph(N+1,vector<int>(N+1,INT_MAX)); for(int i=1;i<=N;++i) graph[i][i] = 0; for(auto e : times) graph[e[0]][e[1]] = e[2]; vector<int>d(N+1,INT_MAX); // SPFA queue<int>q; // 顶点入队 d[K] = 0; q.push(K); while(!q.empty()) { int p = q.front(); q.pop(); for(int i=1;i<=N;++i) { if(d[p]!=INT_MAX && graph[p][i]!=INT_MAX && d[p] + graph[p][i] < d[i]) { // 松弛 d[i] = d[p] + graph[p][i]; // 距离有更新的顶点入队 q.push(i); } } } int ans = INT_MIN; for(int i=1;i<=N;++i) { if(d[i]==INT_MAX) return -1; ans = max(ans,d[i]); } return ans; } };
Floyd算法
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { // 建图 vector<vector<int>>graph(N+1,vector<int>(N+1,INT_MAX)); for(int i=1;i<=N;++i) graph[i][i] = 0; for(auto e : times) graph[e[0]][e[1]] = e[2]; // Floyd // 枚举所有顶点依次作为中介点 for(int k=1;k<=N;++k) { // 看点对i,j能否以k作中介进行松弛 for(int i=1;i<=N;++i) for(int j=1;j<=N;++j) { if(graph[i][k]!=INT_MAX && graph[k][j]!=INT_MAX && graph[i][k] + graph[k][j] < graph[i][j]) graph[i][j] = graph[i][k] + graph[k][j]; } } int ans = INT_MIN; for(int i=1;i<=N;++i) { if(graph[K][i]==INT_MAX) return -1; ans = max(ans,graph[K][i]); } return ans; } };