Floyd-Warshall(弗洛伊德)
求解所有两点间的最短路问题的问题叫做任意两点的最短路问题.这个问题也被称为多源最短路径问题.
今天我们讨论的Floy-Warshall就是多源最短路问题.下面来看看挑战编程程序设计竞赛对它的叙述:
- 我的理解:
- 我们一共有v个点,对于每个点我们都可以决定选不选择这个点去更新最短路.当我们遇到一个点的时候我们不选择用这个点去更新,那么当前的最短路就是:d[i][j]=dp[i][j];当我们用这个点去更新的时候就是:d[i][j]=d[i][k]+d[k][j];d[i][j]表示i->j当前的最短路.我们遍历每一个点那么我们得到的结果就是:两种情况中最小的那一个.程序样例(只有四行的代码)在下方给出.
- 我们一共有v个点,对于每个点我们都可以决定选不选择这个点去更新最短路.当我们遇到一个点的时候我们不选择用这个点去更新,那么当前的最短路就是:d[i][j]=dp[i][j];当我们用这个点去更新的时候就是:d[i][j]=d[i][k]+d[k][j];d[i][j]表示i->j当前的最短路.我们遍历每一个点那么我们得到的结果就是:两种情况中最小的那一个.程序样例(只有四行的代码)在下方给出.
for(int k=1;k<=v;k++){
for(int i=1;i<=v;i++){
for(int j=1;j<=v;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);///全部初始化为inf,i==j时初始化为0
}
}
Dijkstra(迪杰斯特拉)
求解一个点(源点)到其余各个顶点的最短路径,也叫做单源最短路径问题.
假设我们现在有两个集合(已经使用过的点,未使用的点),首先我们把顶点A放进已经使用的点,然后我们找到所有与顶点A相连的边.查找到距离A最短的边AB=2,然后将这个新的顶点B加入到已经使用过的点的集合中,并且去更新其他点的最短距离(最短距离均指的是距离源点的最短距离).等到到B时,我们再次找到d数组中最小的那个数(距离已经使用过的点的集合最近的那个边),对应图中也就是C点,我们在把C点加入到已经使用过的点的集合中,并且去更新其他点的最短距离.后面的点重复这个过程,我们每次增加一个点,那么在n-1次之后所有的点都进入到了已经使用过的点的集合中.此时源点(这里是A)到每一个点的最短距离也就找到了.
- 使用邻接矩的方法来实现Dijkstra.
//邻接矩阵需要注意输入是否存在重边的情况
for(int i=1;i<=n-1;i++){
int mmin=inf;
//此步可以优化
for(int j=1;j<=n;j++){
if(!vis[j]&&dis[j]<mmin){//vis记录此时这个点是否已经加入使用的集合
mmin=dis[j];//dis初始值是刚开始1到各个点的距离
u=j;//找到距离已经使用过的点的集合最近的点
}
}
vis[u]=1;
for(int v=1;v<=n;v++){//使用选到这个点,去更新其他点的最短距离
if(a[u][v]!=inf&&dis[v]>dis[u]+a[i][j])
dis[v]=dis[u]+a[u][v];
}
}
通过上面的代码我们可以看得出来,这个算法的时间复杂度是O(N2).我们每次找到距离顶点A最近的顶点的时间复杂度是O(N),这里我们可以用优先队列来优化,我们可以将它优化成O(logN).而且我们在更新边的时候,每次查顶点的时候,我们都把边遍历了一边,对于边数少的稀疏图来说我们可以使用邻接表的方法,这样我们更新最短距离的时候,每条边只要访问一次就行了.使用邻接表的时间复杂度O(MlogN).如果边特别多的话,时间复杂度比邻接矩阵还高(M<=N2).挑战程序设计竞赛中对于时间复杂度的解释:
typedef pair<int,int> P;//first:距离,second:点
priority_queue<P>q;
vector<Edge>G[Max_n];
int dis[Max_n];
void Dijkstra(){
for(int i=1;i<=n;i++) dis[i]=inf;
q.push(P(0,1));
while(!q.empty()){
P p=q.top();q.pop();
int v=p.second;
if(dis[v]<p.first) continue;//取出的不是最短距离就跳过
for(int i=0;i<G[v].size();i++){
Edge e=G[v][i];//遍历这个点能够到达的所有的边,如果最短距离可以更新旧更新
if(dis[e.to]>dis[v]+e.cost){
dis[e.to]=dis[v]+e.cost;
q.push(P(dis[e.to],e.to));
}
}
}
}
邻接表过程模拟:
Bellman-Ford(贝尔曼·福特)
Dijkstra算法虽然复杂度低,但是如果一个图中含有负权边就没有办法处理了.接下来我们说到的就是Bellman-Ford算法,他的实现方法很简单,并且可以处理带有负权边的图.他的核心就是:对所有边进行n-1次“松弛”操作.因为最短路径上最多有n-1条边,所有这个算法最多执行n-1次,在每一个阶段,我们对每一条边都进行松弛操作,在k个阶段结束后,就已经找出从源点出发“最多经过k条边”到达各个顶点的最短路.直到进行完n-1个阶段后,便得出了最多n-1条边的最短路径.前面提到这个算法还可以检测一个图是否存在负权边.最短路径所包含的边最多为n-1条,即进行n-1轮松弛之后最短路不会再发生变化.这就是说如果在n-1轮之后,我们在对所有边进行一次松弛,如果还可以进行松弛,那么说明图中存在负权圈.
所以在使用Bellman-Ford的时候,我们可以有一个发现:有的时候,在没有进行n-1轮操作之前我们就已经把所有的最短路都已经更新出来了,其实前面的n-1都是一个最大值,两点之前最多有n-1条边,我们最多松弛n-1次,但是大多数的时候,我们不用更新这么多轮就已经可以把所有的点更新出来了.所有我们可以添加一个check来检查最短路是否发生了更新,如果没有发生更新,就可以提前跳出循环了.
//判断回路中是否存在负权圈
bool bellman_ford(){
for(int i=1;i<=n-1;i++){///n个点
bool check=false;///标记点是否发生了更新
for(int j=1;j<=m;j++){///m条边
if(dis[v[j]]>dis[u[j]]+cost[j]){
dis[v[j]]=dis[u[j]]+cost[j];
check=true;
}
}
if(!check) break;///没有发生更新就跳出
}
bool flag=false;
for(int i=1;i<=m;i++){
if(dis[v[i]]>dis[[u[i]]+cost[i]){
dis[v[i]]=dis[u[i]]+cost[i];
flag=true;
}
}
return flag;
}
Bellman-Ford队列优化(SPFA算法)
我们知道了Bellman-Ford算法的大致过程,接下来我们会发现:在每实施一次松弛操作后,就会有一些顶点已经求得其最短路,以后这些顶点的最短路的估计值就会一直保持不变,不在受到后续松弛操作的影响,但是每次还是要判断是否需要松弛,这里就浪费了时间.我们可以对其做出的优化就是每次仅对最短路估计值发生变化的顶点的所有出边进行松弛操作.
算法模拟:
实现过程:
判断负权圈及总结:
关于这里面讲到的一个点入队n次就能判断这个图中含有负权圈,我自己也不是很理解为什么是n而不是n-1,不过可以确定的是,一个点入队的次数肯定是不可能超过n次的,如果超过了,那么这个图一定存在负权圈.在spfa中,如果一个顶点入队的次数超过n,则表示有向图中存在负权值回路.原理:如果存在负权值回路,那么从源点到某个顶点的最短路径可以无限缩短,某些顶点入队将超过n次.
typedef pair<int,int>P;
const int inf=0x3f3f3f3f;
const int Max_n=(int)1e4+10;
int dis[Max_n],in[Max_n];
bool vis[Max_n];
vector<P>G[Max_n];
queue<int>q;
bool spfa(int n){
//多组输入时,不能忘记清空队列和邻接表
for(int i=1;i<=n;i++) dis[i]=inf;
dis[1]=0;
memset(vis,0,sizeof(vis));
memset(in,0,sizeof(in));
vis[1]=true;
q.push(1);
while(!q.empty()){
int now=q.front();q.pop();vis[now]=false;//拿出来取消标记
in[now]++;
if(in[now]>n) return false;//有负权圈
for(int i=0;i<G[now].size();i++){
P p=G[now][i];
if(dis[p.first]>dis[now]+p.second){
dis[p.first]=dis[now]+p.second;
if(!vis[p.first]){
vis[p.first]=true;//放进去时进行标记(优化)
q.push(p.first);
}
}
}
}
return true;//无负权圈
}