Floyd算法
算法特点
首先请出的是最短路算法中的最容易实现的算法Floyd算法,其解决的问题为多源最短路,即图上任意两点间的最短路问题。
floyd算法的时间复杂度较高,但换来的是较易的实现和较小的常数
适用范围
适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)(来自oiwiki)
算法实现
设数组f[x][y]表示从x到y的最短路,每次分别枚举中间点k,起点x,终点y,于是f[x][y]可以由min(f[x][k]+f[k][y])转移出来。
对于该方程有的同学可能有些疑惑,枚举起点x和y的顺序因为是对称的对答案没有影响,但枚举中间点k是否对答案有影响呢?
仔细分析每个枚举过程,可知每个枚举的中间点k一定可以松弛出前k-1个已经枚举过后的最短路,即第k个中间点的松弛操作是完全建立在前k-1个中间点的松弛操作后的,该松弛操作可以求出目前能松弛的所以最短路,所以对于每条最短路,其中一定有最后一个点x能经过松弛操作链接两条最短路,所以不必担心枚举k顺序对答案的影响
回到实现可知该算法需要枚举每个中间点,每个起点,终点,所以有三层循环,时间复杂度为O(n3)
代码实现
for(int k=1;k<=n;++k){//枚举中间点
for(int x=1;x<=n;++x){//枚举起点
for(int y=1;y<=n;++y){//枚举终点
d[x][y]=min(d[x][y],d[x][k]+d[k][y]);
}
}
}
Bellman-Ford算法
算法特点
是基于松弛操作的算法,可以实现单源最短路的求取,能判断图是否含有负环
适用范围
可以适用于含有负边权的图,并能够判断最短路是否存在(是否有负环)
算法实现
松弛操作: dis[u]=min(dis[u],dis[v]+val[v]) 即对每一条路进行松弛操作,看其是否可以通过另一个更小的点转移过来。这是很好理解的,每次遍历整个图,如果有边能松弛就进行松弛操作,最后无法松弛时得到的就是最短路。
负环判断:考虑每次进行松弛操作,如果成功松弛,那么对于一条最短路会增加至少一条边,而一条最短路最多有n-1条边,所以如果松弛操作成功进行了n轮及以上,说明该图含有负环。
考虑时间复杂度,每次遍历的时间复杂度为O(m),如果没有负环的情况下最多遍历n次,总的时间复杂度为O(n*m)
代码实现
struct edge{
int u,v,val;
// u起点
// v终点
// val边权
}
edge e[m];
const int inf=0x3f3f3f3f;
bool bellman-ford(){
bool flag = false; //判断该轮松弛操作是否成功
memset(dis,0x3f,sizeof(dis)); //初始化所有dis
dis[st] = 0;//起点的dis设为0
for(int i=1;i<=n;++i){ //进行n轮松弛操作
flag = false;
for(int i=1;i<=m;++i){ //遍历每条边
int u=e[i].u,v=e[i].v,val=e[i].val;
if(dis[u]==inf) continue;//如果该点未被松弛,自然也无法松弛其他点
if(dis[v]>dis[u]+val){
flag = true; //松弛成功
dis[v]=dis[u]+val; //更新最短路
}
}
if(!flag){ //如果该轮没有松弛成功,证明已经找出所有最短路,可以跳出循环
break;
}
}
return flag;// 如果flag为flase证明成功找到最短路,图内没有负环,如果flag为true证明第n次操作同样松弛成功,根据上面的证明可知,该图内含有负环.
}
算法优化:SPFA
根据上面的松弛公式不难推出,只有在上一轮被松弛过的终点v才有可能去松弛其他的点,所以我们只要通过队列来存储上次松弛过的节点即可减去不必要边的遍历,但由于卡spfa已成常态,而在最坏情况下spfa的时间复杂度接近于普通的bellman-ford,所以常有spfa已死的说法,在这里就不继续展开spfa算法了。
Dijkstra 算法
算法特点
是求正边权图的单源最短路最常用的算法,而且名字容易叫错
适用范围
是这次介绍算法中唯一一个不能用于有负边权的图的算法
算法实现
1.将已经标记节点舍弃,找出剩下点中,dis最小的点,加入队列
2.对该边终点的所有连边进行松弛操作,并标记该节点
以下面图的例子为例
首先将除了起点外的所有点的dis设为正无穷,并将起点1加入队列
更新1所有连边的dis值
此时dis[2] 变为1,dis[3]变为3,给节点1打上标记。
通过分析dis可知,剩余点中dis值最小的点为节点2.
将2放入队列,更新2所有连边的dis值
接下来发现dis值最小的点为3,将3放入队列,更新节点3所有连边的dis值
最后发现dis值最小的点是节点4,同理对节点4进行操作。
最后所有点都打上标记,没有可以松弛的边,算法结束。
因为每次的dis值是由另一节点的dis值转移过来,所以可证,每次dis最小的节点到源点的最短路已经找出,所以可由他去松弛其他节点
之所以dj算法不能适用于有负边权的图就是因为,在负边权的图中,每次dis最小的节点到源点的最短路不一定找出,可能由更大的dis加上一个负值得出
考虑使用优先队列来维护每次的最小dis值,队列中的元素最多为m个,会进行m次插入删除操作,而优先队列的时间复杂度为O(logm),所以总的时间复杂度为O(mlogm) (这里只给出了优先队列的优化方式,其他优化请参考oiwiki)
代码实现
struct node{
int dis,u;// dis距离 节点
bool operator<(const node& a)const{//优先队列小根堆
return a.dis<dis;
}
}
vector<pair<int,int>>e[N];//存边
int dis[N];
priority_queue<node>q;//优先队列
void dijkstra(){
memset(dis,0x3f,sizeof(dis)) //初始化所有dis
dis[s]=0; //起点的dis设为0
q.push({0,s}); //放入起点
while(q.size()){
int u=q.top().u;// 每次选出dis最小的节点
q.pop();
if(vis[u]) continue; //如果标记过该点就退出
vis[u]=1;
for(auto i:e[u]){// 对该点的所有连边进行松弛操作
int val=i.first,v=i.second;//边权 边的终点
if(dis[v]>dis[u]+val){ //松弛成功,将新的dis加入队列
dis[v]=dis[u]+val;
q.push({dis[v],v});
}
}
}
}