您的位置:首页 > 其它

最短路问题总结

2014-04-03 23:40 218 查看
    最短路问题可以分成单源最短路和全局最短路两类,下面分别介绍。

单源最短路

    单源最短路就是把图中某一个点当做起点,计算从起点到其余各点的最短路径。单源最短路的算法又因为图的特点分成两类:无负边权图的单源最短路和有负边权图的单源最短路。

1、无负边权图的最短路——Dijkstra算法

   


    这个算法是通过为每个顶点 v 保留目前为止所找到的从起点s到v的最短路径来工作的。初始时,起点s到自身 的路径长度值被赋为 0 (d[s] = 0),若存在能直接到达的边 e(s,m) ,则把d[m]设为 e(s,m).value ,同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大,即表示我们不知道任何通向这些顶点的路径(对于图中所有顶点 v 除 s 和上述 m 外 d[v] = ∞)。当算法退出时,d[v] 中存储的便是从 s 到 v 的最短路径,或者如果路径不存在的话是无穷大。 Dijkstra
算法的基础操作是边的拓展:如果存在一条从 u 到 v 的边,那么从 s 到 v 的最短路径可以通过将边 e(u, v) 添加到尾部来拓展一条从 s 到 u 的路径。这条路径的长度是 d[u] + e(u, v).value 。如果这个值比目前已知的 d[v] 的值要小,我们可以用新值来替代当前 d[v] 中的值。拓展边的操作一直运行到所有的 d[v] 都代表从 s 到 v 最短路径的花费。这个算法经过组织因而当 d[u] 达到它最终的值的时候每条边 e(u, v) 都只被拓展一次。

    算法维护两个顶点集 S 和 Q。集合 S 保留了我们已知的所有 d[v] 的值已经是最短路径的值顶点,而集合 Q 则保留其他所有顶点。集合S初始状态为空,而后每一步都有一个顶点从 Q 移动到 S。这个被选择的顶点是 Q 中拥有最小的 d[u] 值的顶点。当一个顶点 u 从 Q 中转移到了 S 中,算法对每条外接边 e(u, v) 都尝试了扩展。

邻接矩阵版本:

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N = 101;
const int INF = 0x7fffffff;                         //int型最大值
struct dis
{
    dis( int V , int C ):v(V),c(C){}
    int v , c;
    bool operator < ( const dis &d )const           //重载小于号,队列中升序排列
    {  
return c > d.c;
}
};
int g

, n , m , d
;                         //建立邻接矩阵、最短路径长度数组
//int pre
;                                       //如果需要具体的最短路径则定义前驱记录数组
bool ok
;                                         //已扩展的点集
void Dijkstra( int s , int e )
{
    priority_queue<dis> q;
    memset(ok,false,sizeof(ok));
    for( int i = 0 ; i <= n ; i++ )
        d[i] = INF;                                 //初始化最短路径长度数组
    d[s] = 0;                                       //初始化起点
    q.push(dis(s,0));                               //起点进入待扩展队列
    while(!q.empty())                               //如果还有未扩展而可扩展的点
    {
        if( ok[q.top().v] )                         //扩展过的点跳过,并出队列
        {
            q.pop();
            continue;
        }
        int v = q.top().v;
        if( v == e )
            return;                                 //如果已经获得到终点的最短路径,跳出算法
        ok[v] = true;                               //标记已扩展过的点
        q.pop();                                    //出队列
        for( int i = 1 ; i <= n ; i++ )             //在未确定的点中寻找newV相邻可扩展的点
            if( !ok[i] && g[v][i] < INF )           //遍历可达的邻接点
            {
                if( d[v]+g[v][i] < d[i] )           //如果可以更新
                {
                    d[i] = d[v]+g[v][i];            //更新邻接点到起点的距离
                    q.push(dis(i,d[i]));            //入队列
                    //pre[i] = v;                   //更新邻接点的前驱(如果需要知道具体的最短路径)
                }
            }
    }
}
int main()
{
    while( scanf("%d%d",&n,&m) , n||m )
    {
        for( int i = 1 ; i <= n ; i++ )
            for( int j = i ; j <= n ; j++ )
                g[i][j] = g[j][i] = INF;            //初始化邻接矩阵
        for( int i = 0 ; i < m ; i++ )
        {
            int a , b , c;
            scanf("%d%d%d",&a,&b,&c);
            if( g[a] > c )                       //不查重可以去掉if
                g[a][b] = g[b][a] = c;              //填写邻接矩阵
        }
        Dijkstra(1,n);                              //输入起点和终点
        printf("%d\n",d
);
    }
    return 0;
}
对应的邻接表版本:
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int N = 101;
const int INF = 0x7fffffff;                         //int型最大值
struct edge
{
    edge(int V, int C):v(V),c(C){}
    int v , c;
    bool operator < ( const edge &e )const          //重载小于号,队列中升序排列
    {
        return c > e.c;
    }
};
vector<edge> g
;                                  //建立邻接表
int n , m , d
;                                   //最短路径长度数组
//int pre
;                                       //如果需要具体的最短路径则定义前驱记录数组
bool ok
;                                         //已扩展的点集
void Dijkstra( int s , int e )
{
    priority_queue<edge> q;                         //这里借用edge类型来存v和d[v]
    memset(ok,false,sizeof(ok));
    for( int i = 0 ; i <= n ; i++ )
        d[i] = INF;                                 //初始化最短路径长度数组
    d[s] = 0;                                       //初始化起点
    q.push(edge(s,0));                              //起点入队列
    while(!q.empty())                               //如果还有未扩展而可扩展的点
    {
        if( ok[q.top().v] )                         //扩展过的点跳过并出队列
        {
            q.pop();
            continue;
        }
        int v = q.top().v;
        if( v == e )
            return;                                 //如果已经获得到终点的最短路径,跳出算法
        ok[v] = true;                               //标记已扩展过的点
        q.pop();                                    //扩展过了就出队列
        for( int i = 0 ; i < g[v].size() ; i++ )    //在未确定的点中寻找newV相邻可扩展的点
            if( !ok[g[v][i].v] )                    //遍历可达的邻接点
            {
                int u = g[v][i].v;
                int c = g[v][i].c;
                if( d[v]+c < d[u] )                 //如果可以更新
                {
                    d[u] = d[v]+c;                  //更新邻接点到起点的距离
                    //pre[u] = v;                   //更新邻接点的前驱(如果需要知道具体的最短路径)
                    q.push(edge(u,d[u]));           //入队列准备扩展
                }
            }
    }
}
int main()
{
    while( scanf("%d%d",&n,&m) , n||m )
    {
        for( int i = 1 ; i <= n ; i++ )
            g[i].clear();                           //初始化邻接矩阵
        for( int i = 0 ; i < m ; i++ )
        {
            int a , b , c;
            scanf("%d%d%d",&a,&b,&c);
            g[a].push_back(edge(b,c));
            g[b].push_back(edge(a,c));
        }
        Dijkstra(1,n);                              //输入起点和终点
        printf("%d\n",d
);
    }
    return 0;
}

2、含负边权的最短路——SPFA算法

【适合处理】

①图含有负边权的情况;

②需要判断无解(有负权环)的情况。

【算法流程】

算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。 直到队列为空时算法结束。

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径,可以处理负边。SPFA的实现甚至比Dijkstra或者Bellman_Ford还要简单:

设 d 代表 s 到 i 点的当前最短距离,pre 代表 s 到 i 的当前最短路径中 i 点之前的一个点的编号。开始时 d 全部为 ∞ ,只有 d[s] = 0,pre全部为0。

维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点 s 。用一个布尔数组记录每个点是否处在队列中。

每次迭代,取出队头的点 v ,依次枚举从 v 出发的边 v->u ,设边的长度为 c ,判断 d[v]+c 是否小于 d[u] ,若小于则改进 d[u] ,pre[u] = v,并且由于 s 到 u 的最短距离变小了,有可能 u 可以改进其它的点,所以若 u 不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是 s 到所有的最短距离都确定下来,结束算法。若一个点入队次数超过n,则有负权环。

SPFA 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。

SPFA算法(Shortest Path Faster Algorithm),也是求解单源最短路径问题的一种算法,用来解决:给定一个加权有向图 G 和源点 s ,对于图 G 中的任意一点 v ,求从 s 到 v 的最短路径。 SPFA算法是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算,他的基本算法和Bellman-Ford一样,并且用如下的方法改进:

1、第二步,不是枚举所有节点,而是通过队列来进行优化 设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点 u ,并且用 u 点当前的最短路径估计值对离开 u 点所指向的结点 v 进行松弛操作,如果 v 点的最短路径估计值有所调整,且 v 点不在当前的队列中,就将 v 点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

2、同时除了通过判断队列是否为空来结束循环,还可以通过下面的方法: 判断有无负环:如果某个点进入队列的次数超过 V 次则存在负环(SPFA无法处理带负环的图)。

①松弛

每次松弛操作实际上是对相邻节点的访问,第 n 次松弛操作保证了所有深度为 n 的路径最短。由于图的最短路径最长不会经过超过 V-1 条边,所以可知贝尔曼-福特算法所得为最短路径。

②负权环判定

因为负权环可以无限制的降低总花费,所以如果发现第 n 次操作仍可降低花销,就一定存在负权环。

【算法优化】

SPFA算法有两个优化算法 SLF 和 LLL:

SLF:Small Label First 策略,设要加入的节点是 j ,队首元素为 i ,若d[j] < d[i] ,则将 j 插入队首,否则插入队尾。

LLL:Large Label Last 策略,设队首元素为 i ,队列中所有 d 值的平均值为 x ,若d[i] > x 则将 i 插入到队尾,查找下一元素,直到找到某一 i 使得d[i] <= x,则将 i 出对进行松弛操作。

SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,只要没有负边权,通常使用效率更加稳定的Dijkstra算法。

邻接矩阵:

#include<cstdio>
#include<cstring>
#include<deque>
using namespace std;
const int N = 200;
const int INF = 0x7fffffff;
int g

, n , m , d
;
bool SPFA( int s , int e )
{
int v , cnt
= {0};
bool in_q
= {false};
deque<int> q;
for( int i = 0 ; i < n ; i++ )
d[i] = INF;                                        //最短路径长初始化
q.push_back(s);                                        //起点初始化
d[s] = 0;
cnt[s]++;
in_q[s] = true;
while(!q.empty())
{
v = q.front();
q.pop_front();
for( int i = 0 ; i < n ; i++ )
if( g[v][i] < INF && d[v]+g[v][i] < d[i] )     //松弛
{
d[i] = d[v]+g[v][i];
if( !in_q[i] )                             //入队列
{
in_q[i] = true;
cnt[i]++;
if( cnt[i] > n )                       //判断负权环
return false;
if( !q.empty() && d[i] > d[q.front()] )//SLF优化
q.push_back(i);
else
q.push_front(i);
}
}
in_q[v] = false;
}
return d[e] != INF;
}
int main()
{
int a , b , c;
while( ~scanf("%d%d",&n,&m) )
{
for( int i = 0 ; i < n ; i++ )
for( int j = 0 ; j <= i ; j++ )
g[i][j] = g[j][i] = INF;                   //邻接矩阵初始化
for( int i = 0 ; i < m ; i++ )
{
scanf("%d%d%d",&a,&b,&c);
if( g[a][b] > c )
g[a][b] = g[b][a] = c;
}
scanf("%d%d",&a,&b);
printf("%d\n",SPFA(a,b)?d[b]:-1 );
}
return 0;
}
邻接表(就不写注释了):
#include<cstdio>
#include<cstring>
#include<deque>
#include<vector>
using namespace std;
const int N = 200;
const int INF = 0x7fffffff;
struct edge
{
edge( int V , int C ):v(V),c(C){}
int v , c;
};
vector<edge> g
;
int n , m , d
;
bool SPFA( int s , int e )
{
int v , cnt
= {0};
bool in_q
= {false};
deque<int> q;
for( int i = 0 ; i < n ; i++ )
d[i] = INF;
q.push_back(s);
d[s] = 0;
cnt[s]++;
in_q[s] = true;
while(!q.empty())
{
v = q.front();
q.pop_front();
for( int i = 0 ; i < g[v].size() ; i++ )
{
int u = g[v][i].v;
int c = g[v][i].c;
if( d[v]+c < d[u] )
{
d[u] = d[v]+c;
if( !in_q[u] )
{
in_q[u] = true;
cnt[u]++;
if( cnt[u] > n )
return false;
if( !q.empty() && d[u] > d[q.front()] )
q.push_back(u);
else
q.push_front(u);
}
}
}
in_q[v] = false;
}
return d[e] != INF;
}
int main()
{
int a , b , c;
bool in;
while( ~scanf("%d%d",&n,&m) )
{
for( int i = 0 ; i < n ; i++ )
g[i].clear();
for( int i = 0 ; i < m ; i++ )
{
scanf("%d%d%d",&a,&b,&c);
in = true;
for( int j = 0 ; j < g[a].size() ; j++ )
if( g[a][j].v == b )
{
in = false;
if( g[a][j].c > c )
in = true;
break;
}
if( in )
{
g[a].push_back(edge(b,c));
g[b].push_back(edge(a,c));
}
}
scanf("%d%d",&a,&b);
printf("%d\n",SPFA(a,b)?d[b]:-1 );
}
return 0;
}
SPFA用邻接表,没有在内部解决重边的问题,只好事先做判断,虽然这对vector存储的邻接表来说很麻烦。

全局最短路

    全局最短路即求图中每一点到其他所有点的最短路。不管是带负边权还是不带,只有Floyd算法这一种解决方案。

Floyd-Warshall算法

Floyd-Warshall算法,简称Floyd算法,用于求解任意两点间的最短距离,时间复杂度为O(n^3)。我们平时所见的Floyd算法的一般形式如下:

void Floyd()
{
int i,j,k;
for( k = 1 ; k <= n ; k++ )
for( i = 1 ; i <= n ; i++ )
for( j = 1 ; j <= n ; j++ )
if( d[i][k]+d[k][j] < d[i][j] )
d[i][j] = d[i][k]+d[k][j];
}
注意下第6行这个地方,如果 d[i][k] 或者 d[k][j] 不存在,程序中用一个很大的数代替。最好写成
if( d[i][k] != INF && d[k][j] != INF && d[i][k]+d[k][j] < d[i][j] )
上面这个形式的算法其实是Floyd算法的精简版,而真正的Floyd算法是一种基于DP(Dynamic Programming)的最短路径算法。

设图G中n 个顶点的编号为1到n。令 d[ i , j , k ] 表示从 i  到 j  的最短路径的长度,其中 k 表示该路径中的最大顶点,也就是说 d[ i , j , k ] 这条最短路径所通过的中间顶点最大不超过 k 。

因此:

    若G中包含边,则 d[ i , j , 0 ] = 边的长度;

    若 i = j ,则 d[ i , j , 0 ] = 0;

    若G中不包含边,则 d[ i , j , 0 ] = +∞ 。 d[ i , j , n ]  则是从 i  到 j  的最短路径的长度。

对于任意的 k > 0 ,通过分析可以得到:

    中间顶点不超过 k 的 i 到 j 的最短路径有两种可能:该路径含或不含中间顶点 k 。

    若不含,则该路径长度应为 d[ i , j , k-1 ],否则长度为 d[ i , k , k-1 ]+d[ k , j , k-1 ]。d[ i , j , k ]可取两者中的最小值。

    于是有DP的状态转移方程:[b]d[ i , j , k ] = min{ d[ i , j , k-1 ] , d[ i , k , k-1 ]+d[ k , j , k-1 ] },k>0。


这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。



为了进一步理解,观察上面这个有向图:

    若 k = 0, 1, 2, 3,则 d[ 1 , 3 , k ] = +∞ ,d[ 1 , 3 , 4 ] = 28;

    若 k = 5, 6, 7,则 d[ 1 , 3 , k ] = 10;

    若 k = 8, 9, 10,则 d[ 1 , 3 , k ] = 9。

因此1到3的最短路径长度为9。

不输出路径:

#include<cstdio>
const int N = 200;
const int INF = 0x7fffffff;
int g

, n , m , d

;
void Floyd()
{
int i , j , k;
for( i = 0 ; i < n ; i++ )
for( j = 0 ; j < n ; j++ )
d[i][j] = g[i][j];
for( k = 0 ; k < n ; k++ )
for( i = 0 ; i < n ; i++ )
for( j = 0 ; j < n ; j++ )
if( d[i][k] < INF && d[k][j] < INF && d[i][k]+d[k][j] < d[i][j] )
d[i][j] = d[i][k]+d[k][j];
}
int main()
{
int a , b , c;
while( ~scanf("%d%d",&n,&m) )
{
for( int i = 0 ; i < n ; i++ )
for( int j = 0 ; j <= i ; j++ )
g[i][j] = g[j][i] = j<i?INF:0;
for( int i = 0 ; i < m ; i++ )
{
scanf("%d%d%d",&a,&b,&c);
if( g[a] > c )
g[a][b] = g[b][a] = c;
}
Floyd();
scanf("%d%d",&a,&b);
printf("%d\n",d[a][b]<INF?d[a][b]:-1 );
}
return 0;
}
输出路径:
#include<cstdio>
const int N = 200;
const int INF = 0x7fffffff;
int g

, n , m , d

, pre

, path
, foot;
void Floyd()
{
int i , j , k;
for( i = 0 ; i < n ; i++ )
for( j = 0 ; j < n ; j++ )
{
d[i][j] = g[i][j];
pre[i][j] = 0;
}
for( k = 0 ; k < n ; k++ )
for( i = 0 ; i < n ; i++ )
for( j = 0 ; j < n ; j++ )
if( d[i][k] < INF && d[k][j] < INF && d[i][k]+d[k][j] < d[i][j] )
{
d[i][j] = d[i][k]+d[k][j];
pre[i][j] = k;
}
}
void make_path( int i , int j )
{
if( pre[i][j] > 0 )
{
make_path( i , pre[i][j] );
make_path( pre[i][j] , j );
}
else
path[foot++] = j;
}
int main()
{
int a , b , c;
while( ~scanf("%d%d",&n,&m) )
{
for( int i = 0 ; i < n ; i++ )
for( int j = 0 ; j <= i ; j++ )
g[i][j] = g[j][i] = j<i?INF:0;
for( int i = 0 ; i < m ; i++ )
{
scanf("%d%d%d",&a,&b,&c);
if( g[a][b] > c )
g[a][b] = g[b][a] = c;
}
Floyd();
scanf("%d%d",&a,&b);
if( d[a][b] < INF )
{
printf("cost:%d\n",d[a][b]);
foot = 0;
make_path(a,b);
printf("path:%d->",a );
for( int i = 0 ; i < foot ; i++ )
printf("%d%s",path[i],i==foot-1?"\n":"->");
}
else
printf("no path\n");
}
return 0;
}


[b]这个算法还可以用来计算传递闭包(可达矩阵)

计算闭包只需将Floyd中的d数组改为布尔数组,将加号改为 “|” 就可以了。
for( int k = 0 ; k < n ; k++ )
for( int i = 0 ; i < n ; i++ )
for( int j = 0 ; j < n ; j++ )
d[i][j] |= d[i][k] && d[k][j];
则d为可达矩阵。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息