最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法详解&BFS

1、Bellman-Ford算法
2、Dijkstra算法(代码 以邻接矩阵为例) && Dijkstra + 优先队列的优化(也就是堆优化)
3、floyd-Warshall算法(代码 以邻接矩阵为例)
4、SPFA(代码 以前向星为例)
5、BFS 求解最短路+路径还原
松弛操作:

if(dis[i]>dis[k]+G[k][i])//其中dis[i]是其他的路径,dis[k]是现在的最小路径,G[k][i]是现在的最小路径的点到其他路径点的权值。
{
 dis[i] = dis[k]+G[k][i];
 }

源点用s表示,用数组 dis[N] 来存储最短路径,dis[N]数组为源点到其他点的最小距离。那么最最开始的最短路径的估计值也就是对 dis[N] 的初始化喽。一般我们的初始化都是初始化为 dis[N] = +∞ , But 在一些时候是初始化为dis[N] = 0的(“一些时候”后面再讲),dis[s] = 0;

fill(dis,dis+n,MAX);//不知此函数的可以百度
dis[s] = 0;

Bellman-Ford算法:解决的是一般情况下的单源最短路径问题,其边可以为负值。bellman-ford算法可以判断图是否存在负环,若存在负环会返回一个布尔值。当然在没有负环存在的 情况下会返回所求的最短路径的值。算法如下:
1)图的初始化等操作
2)for i = 1 to |G.V| – 1 //|G.V| 为图 G的点的总数(图中任一点最长的路径(边数)最大为 |G.V| – 1 )
3)for each edge(u,v)∈G.E //G.E 为图 G 的边
4) relax(u,v,w) //也就是if v.d>u.d+w(u,v) , v.d = u.d+w(u,v);
5)for each edge(u,v)∈G.E
6) if v.d>u.d+w(u,v) //v.d为出发源点到结点v的最短路径的估计值 u.d亦如此 w(u,v) 为u结点到v结点的权重值(通俗点就是u—>v的花费)。
7 return false;
8 return true
此算法分为3步:
1)第1行对图进行初始化,初始化dis[N] = +∞,dis[s] = 0;
2) 第2~4行为求最短路的过程,是对图的每一条边进行|V|-1次松弛操作,求取最短路径。
3)第5~8行为对每条边进行|V|-1次松弛后,检查是否存在负环并返回相应的布尔值,因为进行|V|-1次松弛后若没有负环则v.d的值确定不变,若有负环则会继续进行松弛操作,因为一个数+负数是一定比它本身要小的。
此算法的 时间复杂度为O(VE)。
用到的数据结构:

struct Edge
2 {
3     int u, v, w;//u 为起点,v为终点,w为u—>v的权值
4 }edge[maxn;

当已经明确没有负环的时候:

void bellman_ford()
{
     bool flag;//用于优化的
     int dis[maxn];//保存最短路径
     //初始化
     fill(dis,dis+n,INF);//其他点为+∞
     dis[s] = 0;//源点初始化为0
     //m = m<<1;//若为无向图,边的数量应该变为2倍 
     for(int i=1;i<n;i++)//进行|V|-1次
     {
         flag = false;//刚刚开始标记为假
         for(int j=0;j<m;j++)//对每个边
         {   
             //if (v.d>u.d+w(u,v))
             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛操作
             {
                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功
                 flag = true;//若松弛成功则标记为真
             }
         }
         if(!flag)//若所有的边i的循环中没有松弛成功的
             break;//退出循环
         //此优化可以大大提高效率。
     }
     printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
 }

对于优化的解释:若图中存在负环的情况下外循环需要|V|-1次循环,若不存在负环,平均情况下的循环次数是要小于|V|-1次,当所有边没有松弛操作的时候我们就得到了最后的答案,没有必要继续循环下去,So有了这个简单的优化。

当需要判断有没有负环的时候:
方法一:

bool bellman_ford(int s)
{
     bool flag;
     int dis[maxn];//保存最短路径
     fill(dis,dis+n,INF);//初始化
     dis[s] = 0;
     for(int i=1;i<n;i++)//共需进行|V|-1次
     {
         flag = false;//优化 初始化为假
         for(int j=0;j<m;j++)//对每一条边
         {
             // if u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛
             {
                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
                 flag = true;//松弛成功变为真
             }
         }
         if(!flag)//若每条边没有松弛
             break;//跳出循环
     }
     // 一下部分为 3) 第5~8行的操作
     for(int i=0;i<m;i++)
         if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后 有边还能进行松弛 说明
             return true;//存在负环
     return false;//不存在负环
 }

方法二:

bool bellman_ford()
{
    bool flag;
    int dis[maxn];//保存最短路径
    fill(dis,dis+n,INF);//初始化
    dis[s] = 0;
    int i;
    for(i=0;i<n;i++)//共需进行|V|-1次
    {
        flag = false;//优化 初始化为假
        for(int j=0;j<m;j++)//对每一条边
        {
            // if u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
            if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛
            {
                dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
                flag = true;//松弛成功变为真
             }
        }
        if(!flag)//若每条边没有松弛
            break;//跳出循环
         //因为对于V个点 你最多需要进行|V|-1次外循环,如果有负环它会一直进行下去,但是只要进行到第V次的时候就说明存在负环了
        if(i == n-1)//若有
            return true;//返回有负环
    }
    return false;//不存在负环
}

Dijkstra算法:解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为正值。Dijkstra算法在运行过程中维持的关键信息是一组结点集合S。 从源结点s 到该集合中每个结点之间的最短路径都已经被找到。算法重复从结点集V-S中选择最短路径估计最小的结点u,讲u加入到 集合S,然后对所有从u发出的边进行松弛。Dijkstra 算法如下:
1 对图的建立和处理,dis[N]数组的初始化等等操作
2 S = ∅
3 Q = G.V
4 while Q ≠ ∅
5 u = EXTRACT-MIN(Q)
6 S = S ∪ {u}
7 for each vertex v∈ G.Adj[u]
8 relax(u,v,w)
此算法在此分为二步 : 第二大步中又分为3小步
1) 第1~3行 对dis[N]数组等的初始化,集合S 为∅,Q集合为G.V操作
2) 第4~8行 中,
① 第4行 进行G.V次操作
② 第5~6行 从Q中找到一个点,这个点是Q中所有的点u—>S中某点最小的最短路径的点,并将此点加入S集合
③ 第7~8行 进行松弛操作,用此点来更新与u相连的点的路径的距离。
对于邻接矩阵存储的图 来说此算法的时间复杂度为 O(|V|²),用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。
方法一:应用的数据结构:二维数组

int G[maxn][maxn];
void dijkstra()
{
    bool vis[maxn];//相当于集合Q的功能, 标记该点是否访问过
    int dis[maxn];//保存最短路径
    for(int i=0;i<n;i++)//初始化
        dis[i] = G[s][i];//s->其余各个点的距离
    memset(vis,false,sizeof(vis));//初始化为假表示未访问过
    dis[s] = 0;//s->s 距离为0
    vis[s] = true;//s点访问过了,标记为真
    for(int i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作
    {
        int k = -1;
        for(int j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点
            if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的
                k = j;
        if(k == -1)//若图是不连通的则提前结束
            break;//跳出循环
        vis[k] = true;//将k点标记为访问过了
        for(int j=0;j<n;j++)//松弛操作
            if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛
               dis[j] = dis[k]+G[k][j];//j点的距离 大于当前点的距离+w(k,j) 则松弛成功,进行更新
    }
    printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
}

方法二:用到的数据结构 :前向星 对组 优先队列

typedef pair<int, int >P; //pair 的first 保存的为最短距离, second保存的为顶点编号
struct Edge//前向星存边
{
    int to, w;//to为到达的点, w为权重
    int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的
}edge[maxn];//存所有的边
int head[maxn];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入edge的且与u相连的边在edge的位置,即下标
priority_queue<P,vector<P>,greater<P> >que;//优先队列从小到大

前向星的加边函数:

void add(int u, int v, int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];//获得下一个结构体的位置
    head[u] = cnt++;//记录头指针的下标
}

算法实现:

void dijkstra()
{
    int dis[maxn];//最短路径数组
    int v;//v保存从队列中取出的数的第二个数 ,也就是顶点的编号
    priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大
    Edge e;//保存边的信息,为了书写方便
    P p;//保存从队列取出的数值 
    fill(dis,dis+n,MAX);//初始化,都为无穷大
    dis[s] = 0;//s—>s 距离为0
    que.push(P(0,s));//放入距离 为0 点为s
    while(!que.empty())
    {
        p = que.top();//取出队列中最短距离最小的对组
        que.pop();//删除
        v = p.second;//获得最短距离最小的顶点编号
        if(dis[v] < p.first)//若取出的不是最短距离
            continue;//则进行下一次循环
        for(int i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历
        {
            e = edge[i];//为了书写的方便。
            if(dis[e.to]>dis[v]+e.w)//进行松弛
            {
                dis[e.to]=dis[v]+e.w;//松弛成功
                que.push(P(dis[e.to],e.to));//讲找到的松弛成功的距离 和顶点放入队列
            }
        }
    }
    printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
}

floyd-Warshall算法(动态规划):是一个很强大的算法,它可以计算任意两点之间的最短路径,其边可以为负值。
证明:
  对于0~k,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不仅过顶点k的情况下,d[k][i][j] = d[k-1][i][j]。通过顶点k的情况,d[k][i][j]= d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以用同一个数组不断进行如下的操作:d[i][j] = min(d[i][j],d[i][k]+d[k][j])的更新来实现。floyd算法的时间复杂度为O(|V|³)。 

void floyd()
{
    for(int k=0;k<n;k++)
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                G[i][j] = min(G[i][j],G[i][k]+G[k][j]);
    printf("%d\n",G[s][t]==INF?-1:G[s][t]);
}

补充一下:对于floyd判断负环是否存在只需检查是否存在d[i][i]是负数的顶点i 即可。

SPFA算法(bellman-ford算法的优化):设立一个先进先出的 队列 用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短 路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
SPFA 是这样判断负环的: 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<
dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定 ,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
1 对图的建立和处理,dis[N]数组的初始化等等操作
2 Q += s //Q 为一个队列 s为源点
3 while Q ≠ ∅//队列不为空
4 u = Q中的点//从Q中取出一个点u
5 把u点标记为为访问过的
6 for each vertex v∈ G.Adj[u]//对所有的边
7 relax(u,v,w)//进行松弛
8 if(v 未被访问过)//若v未被访问过
9 Q += v;//加入队列
此算法分为3部分 :
1) 第1~2行 建图对dis[N]和vis[N]数组等数组进行初始化。 若判断负环需要加一个flag[N]数组,初始化为0,某点 u 若加入Q队列一次,怎flag[u]++,若flag[u]>=n,说明u进入队列的次数大于点的个数,因此此图存在负环,返回一个布尔值。
2) 第3行当队列不为空的时候进行操作
3) 第4~9行 取出Q中的点u ,用u对所有的边进行松弛操作,若松弛成功,判断该点v是否被访问过,若未访问过加入Q队列中。

所用数据结构:前向星 双向队列

struct Edge
{
   int to,w;//to 终点,w 权值
int next;//下一个
}edge[maxn];//前向星
int head[503];//头指针式的数组
int cnt;//下标
deque<int>que;//双向队列

算法实现:


bool SPFA()
{
    int u, v;//u 从Q中取出的点 v找到的点
    int dis[maxn];//保存最短路径
    int flag[maxn];//保存某点加入队列的次数
    bool vis[maxn];//标记数组
    deque<int>que;//双向队列
    fill(dis,dis+n+1,MAX);//初始化
    memset(flag,0,sizeof(flag));//初始化
    memset(vis,false,sizeof(vis));//初始化
    dis[s] = 0;
    que.push_back(1);//将s = 1 加入队列
    while(!que.empty())//当队列不为空
    {
        u = que.front();//从队列中取出一个数
        que.pop_front();//删除
        vis[u] = false;//标记为未访问
        for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找
        {
            v = edge[i].to;//保存点 便于操作
            if(dis[v]>dis[u]+edge[i].w)//进行松弛操作
            {
                dis[v] = dis[u]+edge[i].w;//松弛成功
                if(!vis[v])//若该点未被标记
                {
                    vis[v] = true;//标记为真
                    flag[v]++;//该点入队列次数++
                    if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环
                        return true;//返回有负环
                    //一下为SLF优化
                    if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离
                        que.push_front(v);//将该点放到队首
                    else//不然
                         que.push_back(v);//放入队尾
               }
            }
       }
    }
    return false;//没有负环
} 

BFS 求解最短路+路径打印:采用邻接表或前向星进行图的存储 , 则BFS的时间复杂度为开始的初始化O(V)+BFS操作O(E) = O (V+E)
所用数据结构:前向星 优先队列

struct P
{
    int v, w;//v 顶点 w 最短距离
    bool operator <(const P &a)const
    {
        return a.w < w;//按w从小到大排序
    }
};
priority_queue<P>que;//优先队列 按w从小到大
struct Edge//前向星
{
    int to, w;//v 顶点 w权重
    int next;//下一个位置
}edge[maxn];
int head[maxn];//头指针数组

算法实现:

void BFS()
{
    priority_queue<P>que;//优先队列 按w从小到大
    bool vis[maxn];//标记数组, 标记是否被访问过
    P p, q;
    int v;
    memset(vis,false,sizeof(vis));//初始化
    p.to = s;//顶点为 s
    p.w = 0;//距离为 0
    que.push(p);//放入队列
    while(!que.empty())//队列不为空
    {
        p = que.top();//取出队列的队首
        que.pop();//删除
        if(p.to == t)//若找到终点
        {
            printf("%d\n",p.w);//输出结果
            return ;//返回
        }
        vis[p.to] = true;//此点标记为访问过
        for(i=head[p.to];i!=-1;i=edge[i].next)//查找与该点相连的点
        {
            v = edge[i].to;
            if(vis[v] == false)//若点未被访问过
            {
                q.to = v;//存入结构体
                q.w = p.w+edge[i].w;//距离更新
                que.push(q);//放入队列
            }
        }
    }
    printf("-1\n");//若没有到达终点 输出-1
}
    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/booyoungxu/article/details/50867782
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞