一年前写的最短路的博客写得太一般了,代码也很难看,刚好最近复习到相关知识,所以重新整理一下
下面涉及题目的代码都在力扣上通过。
DFS可以用来求给定两点之间的最短路。即走遍两点间的所有路径,选择路程最小的那一个
我们可以从U出发,记录已经走过的路程。
采用邻接矩阵的方式来表示图:graph[u][v]=w表示u到v之间有一条权重为w的边,若u和v之间无边,则令graph[u][v]=0。
为了防止两点之间有回路导致DFS时一直在回路上绕圈子,我们定义vis数组来记录当前已经访问过的节点,已经访问过的节点不再访问。(回溯的思想)
设一个全局变量MinDistance表示要求的最短路程。
void dfs(vector>& graph,vector& vis,int src,int dest,int distance){
if(src==dest){
//此时得到了起点到终点之间的一条路径,路程为distance
//假设起点和终点之间的最短路为MinDistance,则执行 MinDistance=min(MinDistance,distance)
}
else{
vis[src]=true;
for(int next_node=1;next_node
上面的代码做了优化,因为我们要求的是最短路,那么如果当前走过的路程已经超过了最短的路程,那么没必要再走下去了。
迪杰斯特拉算法用于求单源最短路程问题,即假设图中有N个顶点,其中一个顶点为U,那么我们可以用迪杰斯特拉算法求得顶点U与其他N-1个顶点之前的最短路程。
算法原理:在还未被访问的顶点中选择离U最近的顶点MID,以MID作为中介点,更新U通过MID到达与MID相邻的顶点的最短路程。
这里的更新被称为松弛操作。我们通过MID点更新U到V的最短路程,需要:
if(U到V的最短路 > U到MID的最短路+MID到V的距离)
U到V的最短路 = U到MID的最短路+MID到V的距离
可以发现每次我们选择一个MID,一共有N个节点,除去U不参与选择,则我们要做N-1次相同的操作,每次取出不同的MID。
Leetcode上有对应的题目:743. 网络延迟时间
下面给出该题的题解,使用了迪杰斯特拉算法,核心就是求单源最短路程。如果不想看题目也可以直接看下面的迪杰斯特拉算法的实现。
class Solution {
public:
int networkDelayTime(vector>& times, int N, int K) {
vector> graph(N+1,vector(N+1,101));
for(auto edge:times){
int from=edge[0],to=edge[1],time=edge[2];
graph[from][to]=time;
}
/*迪杰斯特拉算法*/
vector dist=dijkstra(graph,K,N);
int SpeedTime=0;
for(int i=1;i<=N;++i) SpeedTime=max(SpeedTime,dist[i]),cout< dijkstra(vector>& graph,int src,int N){
vector vis(N+1,false);
vector distance(N+1,INT_MAX);
for(int i=1;i<=N;++i) if(graph[src][i]!=101) distance[i]=graph[src][i]; //初始化与src直接相邻的顶点到src的距离
distance[src]=0;
vis[src]=true;
for(int time=1;timedistance[i]) dist=distance[i],node=i;
}
vis[node]=true;
for(int i=1;i<=N;++i){ //更新未被访问的与node相邻的顶点到src的距离
if(graph[node][i]==101) continue;
if(distance[i]>distance[node]+graph[node][i]){
distance[i]=distance[node]+graph[node][i];
}
}
}
return distance;
}
};
个人对迪杰斯特拉算法的理解是:该算法实际上列举了每个顶点到源点的各种可能路径。对于顶点V,若V与[A,B,C]三个顶点相邻,那么从U到V的路径就可以包含了从U到A再从A到V,B、C同理。算法用A,B,C到U的最短路程以及A,B,C到V的距离来更新U到V的最短路程,保证了在所有U到V的可能路径中选择最小的一个。(有点像广度优先搜索,层层向外扩展)
上面的代码中每次查找相邻顶点我们都要遍历所有顶点(从1到N),很没必要,因此我们可以用邻接表的形式来表示图。
即graph[u]表示一个存放与顶点u相邻的顶点的列表,为了记录顶点之间边的权值,我们可以定义一个结构体或者定义一个vector来记录,比如:
struct edge{
int to,distance;
};
vector> graph(n)
--------------------------------------
vector>> graph(n);
其中最内一层vector用来表示相邻顶点和边的距离
这样就可以只遍历相邻的节点。
此外,每次要先选取未被访问的与源点最近的一个顶点,我们也是用遍历所有顶点的方式。这里的本质是要取最值,那我们可以借助优先队列,来使得队首元素始终是未被访问的与源点最近的一个顶点,这个就不用每次都去遍历一遍去查找了。
这引申出了迪杰斯特拉算法的队列实现方式:(下面代码还是上面题目的题解,用了优先队列实现迪杰斯特拉算法)
class Solution {
public:
struct adj{
int to,time;
};
int networkDelayTime(vector>& times, int N, int K) {
vector> graph(N+1);
for(auto edge:times){
int from=edge[0],to=edge[1],time=edge[2];
graph[from].push_back({to,time});
}
/*迪杰斯特拉算法*/
vector dist=dijkstra(graph,K,N);
int SpeedTime=0;
for(int i=1;i<=N;++i) SpeedTime=max(SpeedTime,dist[i]),cout< dijkstra(vector>& graph,int src,int N){
vector vis(N+1,false);
vector distance(N+1,100005); //这里使用INT_MAX要注意计算时可能会整数溢出
distance[src]=0;
//优先队列的元素:pair表示{顶点,顶点到源点的距离}
priority_queue,vector>,greater>> q;
q.push({0,src});
while(!q.empty()){
int node=q.top().second;
q.pop();
if(vis[node]==true) continue;
vis[node]=true;
for(int i=0;idistance[node]+dist){
distance[next_node]=distance[node]+dist;
q.push({distance[next_node],next_node});
}
}
}
return distance;
}
};
上面优先队列的元素采用了pair
算法 | 时间复杂度 |
原始迪杰斯特拉算法 | O(N^2) |
堆优化迪杰斯特拉算法 | O(N*lgN) |
假设图有N个节点,则该算法一共进行N-1次相同类型的操作(每次可以得到一个顶点到源点的最短路),每次操作是对所有边进行松弛,如下:
int from=边的起点,to=边的终点,weight=边的权重
//distance[node]:表示顶点node到源点的最短距离
if(distance[from]
其中distance[from] 个人的理解是:每次对所有边进行松弛操作,则必有一个顶点到源点的最短距离被计算好。共计算N-1次可以保证N-1个顶点的最短路都被计算出来。(实际上可能不用N-1次就全部计算好,N-1次的极端情况可以联想当图的结构像一条长长的链,且源点是链的端点的时候) 这种算法可以检验出权重和为负值的环的有向图。因为计算N-1次已经保证了可以求得每个顶点到源点的最短路程,若再对所有边做一次松弛操作,由于有一个环的权重和为负值,所以会使得已经计算好的各顶点到源点的最短路程变得更小。以此来检验。 如上图,每对所有边松弛一次,各顶点到源点(①)的最短距离都会减小。 核心代码如下: 上面提到的题目也可以用该算法做,但效率很低。因为这个算法本身的效率就不高。 但是该算法还是有好处的,除了上面说的可以检验图是否含有权值和为负值的环之外,该算法在求含有负数权重的边的图时可以得到正确结果,这是迪杰斯特拉算法没有的功能。以下图为例: 上图是用迪杰斯特拉算法求的,distance是各顶点到源点①的最短距离,最终得到的是[0,1,3,6],但实际上④到①的最短路径可以是5。 若用贝尔曼福特算法,每一次对所有边做松弛操作,则每一次至少可以让一个顶点得到其到源点的最短路程。如第一次的时候③到①的最短路就已经计算好了,在此基础上第二次的时候②到①的最短路也可以得到了,第三次的时候④到①的最短路也可以得到了。 该算法是对贝尔曼福特算法的优化,因为当碰到存在负权边的图时,迪杰斯特拉算法不可用,贝尔曼福特算法又效率低下,所以采用SPFA算法会是比较好的选择。 回想贝尔曼福特算法,进行N-1轮循环,每一轮循环对所有边做松弛操作。但其实只有部分边真正可以做松弛操作,就是那些已经计算了到源点的最短路的顶点相连的边。而且这些边中,也只有一部分有真正地用于更新顶点到源点的最短路径。对于没有用于更新顶点到源点的最短路的边,在下一轮的计算中也不会用到。所以我们只需要关注每一轮中,更新了到源点的最短路的顶点相连的边,这样就很大的简化了每次松弛操作的复杂度。 那如何检验权值和为负数的环呢? 因为在贝尔曼福特算法中,每个顶点最多经历N-1松弛就能找到到源点的最短路,SPFA算法是贝尔曼福特算法的优化,所有检验的原理也相同:如果一个顶点被松弛的次数超过N-1次,那就说明图中有权值和为负数的环。 SPFA算法借助队列实现,队列中存放的是经过松弛操作后更新了到源点的最短路的顶点,所以如果一个顶点在队列中出现次数超过N-1次的话就说明图中有权值和为负数的环。 算法实现如下: 主要在于理解inqueue数组的用法,见代码中的注释。 DFS是学习图论时必学的基础。 迪杰斯特拉算法是非常经典的求最短路的算法,掌握其采用优先队列实现的方法更好,算法效率更高,但注意无法正确求解某些带有负权边的图的最短路。 贝尔曼福特可以解带有负权边的图的最短路,但效率实在是太低了,可以采用对贝尔曼福特算法进行优化的SPFA算法。 上述的迪杰斯特拉、贝尔曼福特、SPFA算法都只是用于求单源最短路问题。 就效率来说,一般采用SPFA算法效率会高一些,但这要看图的结构。 就上面提到的题目来说,效率排行是:SPFA>迪杰斯特拉>贝尔曼福特bool BellManFord(vector
四、SPFA算法
bool SPFA(vector
五、小结