最短路径算法

Floyed算法

Floyed-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。 注意单独一条边的路径也不一定是最佳路径。

思想:
从任意一条单边路径开始。所有两点之间的距离是边的权,或者无穷大,如果两点之间没有边相连。
对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

特点:
Floyed算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。Floyed的时间复杂度是O (N3),适用于出现负边权的情况。

算法描述:
初始化:点u、v如果有边相连,则dis[u][v]=w[u][v]。
如果不相连则dis[u][v]=0x7fffffff
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(dis[i][j]>dis[i][k]+dis[k][j])
dis[i][j]=dis[i][k]+dis[k][j];
算法结束:dis[i][j]得出的就是从i到j的最短路径。

#include<iostream>
using namespace std;
const int maxn=9999999;
int n,dis[101][101];
int x,y,z;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
      dis[i][j]=maxn;
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
      {
        cin>>x>>y>>z;
        dis[x][y]=dis[y][x]=z;
      }
    for(int k=1;k<=n;k++)
      for(int i=1;i<=n;i++)
        if(i!=k)
        for(int j=1;j<=n;j++)
        if(j!=k&&j!=i&&dis[i][j]>dis[i][k]+dis[k][j])
        dis[i][j]=dis[i][k]+dis[k][j];
    //根据题目输出答案 
    return 0;
}

算法分析:
三层循环,第一层循环中间点k,第二第三层循环起点终点i、j,算法的思想很容易理解:如果点i到点k的距离加上点k到点j的距离小于原先点i到点j的距离,那么就用这个更短的路径长度来更新原先点i到点j的距离。
我们在初始化时,把不相连的点之间的距离设为一个很大的数,不妨可以看作这两点相隔很远很远,如果两者之间有最短路径的话,就会更新成最短路径的长度。

Dijkstra算法

用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。

思想:
Dijkstra的算法思想,就是一开始将起点到起点的距离标记为0,而后进行n次循环,每次找出一个到起点距离dis[u]最短的点u,将它标记为访问过的点(白点)。随后枚举所有的未访问过的点vi(蓝点),如果以此白点为中转到达蓝点vi的路径dis[u]+w[u][vi]更短的话,这将它作为vi的“更短路径”dis[vi](此时还不确定是不是vi的最短路径)。

特点:
Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。

算法描述:
设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。
a)初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0;
b)For(i=1;i<= n i++)
1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。
2.u标记为已确定最短路径
3.For与u相连的每个未确定最短路径的顶点v
if(dis[u]+w[u][v]< dis[v])
{
dis[v]=dis[u]+w[u][v];
pre[v]=u;
}
c)算法结束:dis[v]为s到v的最短距离;pre[v]为v的前驱节点,用来输出路径。

#include<iostream>
using namespace std;
const int maxn=9999999;
int n,x,y,z,k,minn;
int dis[101],a[101][101];
bool flag[101];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>x>>y>>z;
        a[x][y]=a[y][x]=z;
        dis[i]=maxn;
    }
    dis[1]=0;
    for(int i=1;i<=n;i++)
    {
        minn=maxn;
        k=0;
        for(int j=1;j<=n;j++)
        if(!flag[j]&&dis[j]<minn)
        {
            minn=dis[j];
            k=j;
        }
        if(k==0) break;
        flag[k]=true;
        for(int j=1;j<=n;j++)
        if(a[k][j]!=0&&!flag[j]&&dis[j]>dis[k]+a[k][j])
        dis[j]=dis[k]+a[k][j];
    }
    //根据题目输出答案  
    return 0;
} 

算法分析:
从起点到一个点的最短路径一定会经过至少一个“中转点”(例如下图1到5的最短路径,中转点是2。特殊地,我们认为起点1也是一个“中转点”)。显而易见,如果我们想求出起点到一个点的最短路径,那我们必然要先求出中转点的最短路径(例如我们必须先求出点2 的最短路径后,才能求出从起点到5的最短路径)。
换句话说,如果起点1到某一点V0的最短路径要经过中转点Vi,那么中转点Vi一定是先于V0被确定了最短路径的点。

这里写图片描述 这里写图片描述

我们把点分为两类,一类是已确定最短路径的点,称为“白点”,另一类是未确定最短路径的点,称为“蓝点”。如果我们要求出一个点的最短路径,就是把这个点由蓝点变为白点。从起点到蓝点的最短路径上的中转点在这个时刻只能是白点。

让我们对以上这段枯燥的文字做一番模拟,加深理解。
算法开始时,作为起点的dis[1]=0,其他的点dis[i]=9999999

这里写图片描述

这时dis[2],dis[3],dis[4]被它的最后一个中转点1修改为了最短路径。

这里写图片描述

这时dis[3],dis[5]被它们的最后一个中转点2修改为了最短路径。

这里写图片描述

这时dis[4]也被它的最后一个中转点3修改为了最短路径。

接下来的两轮循环将4、5也变成白点。N轮循环结束后,所有的点的最短路径即能求出。

Dijkstra无法处理边权为负的情况
例如下图
这里写图片描述

2到3的边权值为-4,显然从起点1到3的最短路径是-2(1→2→3),但是dijskstra在第二轮循环开始时会找到当前dis[i]最小的点3,并标记它为白点。
这时的dis[3]=1,然而1却不是从起点到点3的最短路径。因为3已被标记为白点,最短路径值dis[3]不会再被修改了,所以我们在边权存在负数的情况下得到了错误的答案!

SPFA算法

SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它还有一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。

思想:
初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。

特点:
这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。
SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。
算法时间复杂度:O(kE),E是边数。K是常数,平均值为2。

算法描述:
dis[i]记录从起点s到i的最短路径,w[i][j]记录连接i,j的边的长度。pre[v]记录前趋。
team[1..n]为队列,头指针head,尾指针tail。
布尔数组exist[1..n]记录一个点是否现在存在在队列中。
初始化:dis[s]=0,dis[v]=∞(v≠s),memset(exist,false,sizeof(exist));
起点入队team[1]=s; head=0; tail=1;exist[s]=true;
do
{
1、头指针向下移一位,取出指向的点u。
2、exist[u]=false;//已被取出了队列
3、for与u相连的所有点v
if (dis[v]>dis[u]+w[u][v])
{
dis[v]=dis[u]+w[u][v];
pre[v]=u;
if (!exist[v]) //队列中不存在v点,v入队。
{
//尾指针下移一位,v入队;
exist[v]=true;
}

}
while (head < tail);
循环队列:
采用循环队列能够降低队列大小,队列长度只需开到2*n+5即可。

#include<iostream>
using namespace std;
const int maxn=9999999;
int n,x,y,z,head,tail,u;
int dis[101],a[101][101],pre[101],team[100001];
bool exsit[101];
int main()
{
    cin>>n;
    //输入起点,终点
    for(int i=1;i<=n;i++)
    {
        cin>>x>>y>>z;
        a[x][y]=a[y][x]=z;
        dis[i]=maxn;
    }
    dis[1]=0;
    team[1]=1;//起点入队(此处默认起点为1)
    do
    {
        head++;
        u=team[head];
        exsit[u]=false;
        for(int j=1;j<=n;j++)
        if(dis[j]>dis[u]+a[u][j]&&a[u][j])
        {
            dis[j]=dis[u]+a[u][j];
            pre[j]=u;
            if(!exist[j])
            {
                tail++;
                team[tail]=j;
                exsit[j]=true;
            }
        }
    }while(head<tail);
    //根据题目输出答案  
    return 0;
} 
原文地址:https://www.cnblogs.com/cax1165/p/6071020.html