最短路径问题

单源最短路径问题

单源最短路径是固定一个起点,求它到其他所以点的最短路径的问题。终点也固定的点叫做两点之间最短路径问题。因为两点之间的最短路径问题是单源最短路径问题的复杂度相同所以也归类为单元最短路径问题。
用d[i]表示从起点s出发到顶点i的最短路径。
d[i]=mind[j]+(ji)|e=(j,i)E;d[i]=mind[j]+(从j到i的边的权值)|e=(j,i)属于E;
如果给定是图是DAG(无圈有向图),就可以按拓扑序给顶点编号,并利用这个递推关系式计算出d 。在这总情况下,记起点到i的最短距离为d[i],设初始值为:d[s]=0;d[i]=INF 。然后不断使用这条递推关系不断更新d的值,就可以算出新的d。实际上这利用了深度优先搜索的思想。

如下算法为Bellman-Ford算法。如果在图中不存在从s可达的负圈,那么最短路径不会经过同一个顶点两次,while(true)的循环最多执行|v|-1次,因此,复杂度是O(|V|×|E|)O(|V|×|E|)

#include<iostream>

#define MAX_E 1000
#define MAX_V 1000
#define INF 1000000
using namespace std;

struct  edge { int from, to, cost};

edge es[MAX_E];//边
int d[MAX_V];//用来存储最短距离
int V,E; //点和边的数量


//从顶点s出发到所有点的最短距离,通过边来计算。
void shortest_path(int s){
    for(int i=0;i<V;i++){
        d[i]=INF;
    }
    d[s]=0;//s是起点,从起点到起点距离当然是0;
    while(true){
        bool update=false;
        for(int i=0;i<E;i++){
            edge e=es[i];
            if(d[e.from]!=INF && d[e.to]>d[e.from]+e.cost){
                d[e.to]=d[e.from]+e.cost;
                update=true;
            }
        }
        if(!update) break;
    }        
}

反之,如果存在从s可达的负圈,那么在第|v|次循环中也会更新d的值,因此,也卡伊利用这个性质来检查负圈。如果一开始对所以顶点i,都把d[i]初始化为0,那么可以检查出所所有的负圈。

bool findNegativeLoop(){
    memset(d,0, sizeof(d));

    for(int i=0;i<V;i++){
        for(int j=0;j<E;j++){
            edge e=es[i];
            if(d[e.to]>d[e.from]+e.cost){
                d[e.to]=d[e.from]+e.cost;
                if(i==V-1) return false; //如果不存在负圈,那么最多循环V-1次结束,所以这里如果循环到了V次,那么一定
            }
        }
    }
    return true;
}

该算法时间复杂度为:O(|V||E|)O(|V||E|)

单源最短路径2——Dijkstra算法

在没有负边的情况下,在Bellman0Ford算法中,如果d[i]还不是最短距离的话,那么即使进行d[j]=d[i]+从i到j的权值更新,d[j]也不会变成最短距离。而且,即使d[i]没有变化,每一次循环都要检查一次从i出发的所以边。由此可知,算法很费时间。
现在对算法做如下修改:

  • 找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离
  • 此后不需要再关心最短路径已经确认的顶点。

该算法的代码如下:

#include <iostream>
#define MAX_V 1000
#define INF 100000000000

using namespace std;

int cost[MAX_V][MAX_V]; //存储边的权值
int d[MAX_V];//存储最短距离
bool used[MAX_V];//标记该点是否已经用过,使用过表示该点已计算出最短距离
int V; //顶点数量

void dijkstra(int s){
    fill(d,d+V,INF);
    fill(used,used+V;false);
    d[s]=0;

    while(true){
        int v=-1;
        for(int u=0;u<V;u++){ //从未使用的顶点中选择一个距离最短的顶点
            if(! used[u] && (v== -1 || d[u]<d[v])) v=u;
        }

        if(v==-1) break; //v==-1说明所以的顶点都已经计算完成了

        used[v]=true;    //将v标记为以访问过

        for(int u=0;u<V;u++){  //更新与v相连的所以节点
            d[u]=min(d[u],d[v]+cost[v][u]);
        }
    }
}

使用相邻矩阵实现的Dijkstra算法的时间复杂度为O(|V|2)O(|V|2) 。实际上仔细观察会发现,该算法中while每次循环都要先查询距离最短的一个没有被使用的节点。我们如果在插入时就进行排序,那么会可以直接取出最小的值。

优化后的算法如下:

#include <iostream>
#include <vector>
#include <queue>

#define MAX_V 1000
#define INF 100000000000

using namespace std;

struct edge {
    int to, cost;
};

typedef pair<int, int> P;//first中存放距离,second中存放编号

int V;
vector<edge> G[MAX_V];
int d[MAX_V];

void dijkstra(int s) {
    //优先级队列,从小到大排列
    priority_queue<P, vector<P>, greater<P>> que;
    fill(d, d + V, INF);
    d[s] = 0;
    que.push(P(0, s));

    while (!que.empty()) {
        P p = que.top();
        que.pop();
        int v = p.second;
        if (d[v] < p.first) continue;//在这种情况下说明此记录已经作废,例如第一次循环插入了P(3,4);第二次循环又插入了P(2,4),显然,第二次的数据应该
                                                    //替换掉第一次数据,但是由于我们在这里使用了哦=优先级队列存储,如果要替换掉上一次的记录=会比较麻烦,所以我们不去除无效的数据,
                                                    //当从队列中取出数据时再判断是否是无效的数据(在之前应当被替换的数据)。

        for (int i = 0; i < G[v].size(); i++) {
            edge e = G[v][i];
            if (d[e.to] > d[v] + e.cost) {
                d[e.to] = d[v] + e.cost;
                que.push(P(d[e.to], e.to));
            }
        }
    }

}

该算法的时间复杂度为O(|E|×log|V|)O(|E|×log|V|)

上述的代码中,有一条语句if(d[v]<p.first)continue; 该条代码分析如下:
在这种情况下说明此记录已经作废,例如第一次循环插入了P(3,4);第二次循环又插入了P(2,4),显然,第二次的数据应该替换掉第一次数据,但是由于我们在这里使用了哦=优先级队列存储,如果要替换掉上一次的记录=会比较麻烦,所以我们不去除无效的数据,当从队列中取出数据时再判断是否是无效的数据(在之前应当被替换的数据)。

在优化之前的算法中,我们使用了bool used来标记那些最短距离已经被计算出来的节点,但是在优化算法中却没有看到类似的标记,这是为什么呢?其实原因很简单,优化之前的算法中,我们每次选取最短距离的点是从所有的点中选取的,所以到去除掉已经计算过的点;而优化算法中,是从优先级对接列中取出的点,优先级队列中的点是每次循环更新了距离的点,显然已经计算出最近距离的点不会在更新距离,所以就不会再出现在优先级队列中。因此优化算法中并不需要bool used 标记。

任意两点之间的最短距离——Floyd-Warshall算法

Floayd-Warshall算法和Bellman-Ford算法一样可以处理边数为负数的情况,并且可以判断是否有负圈。
算法思路:
问题使用dp(动态规划)的方法来求解。
只使用顶点0~k和i、j的情况下,记i到j的最短路径长度为d[k+1][i][j] 。k=-1时认为只使用i和j,所以d[0][i][j]=cost[i][j] 。接下来我们将只使用0~k的问题规约到只是用0~k-1的情况。
只使用0~k的情况,我们可以将其分为两种情况:
1、进过顶点k一次。在这种情况下d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]
2、完全不经过顶点k。d[k][i][j]=d[k-1][i][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]);

算法如下:

#include <iostream>
#include <vector>
#include <queue>

#define MAX_V 1000
#define INF 100000000000

using namespace std;

int d[MAX_V][MAX_V];  //d[u][v]表示边 e(u,v)的权值,边不存在设为INF
int V;

void floyd(){
    for(int k=0;k<V;k++){
        for(int i=0;i<V;i++){
            for(int j=0;j<V;j++){
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
            }
        }
    }
}

算法分析:
最外层的循环V次,依次表示使用0~0号点、0~1号点、0~2号点……0~k号点的情况。
假设我们有一个4个节点的图,该图的边信息如下矩阵:
初始矩阵

k=0时,表示只使用0号节点,d[i][j]=min(d[i][j], d[i][0]+d[0][j])
k=0

k=1时,表示只使用0,1号节点,由于k=0时,d[i][i]已经计算完毕,所以此时计算d[i][j],可以以使用公式d[i][j]=(d[i][j], d[i][1]+d[1][j])。
k=1

k=2时:
k=2
k=3时:
k=3

观察上述的计算过程可以发现,当k=a时,矩阵的第a行和第a列不会发生变化,其他位位置的值d[i][j]=min(d[i][j], d[i][a]+d[a][j] )。

路径还原

前面的算法都是在求解最短的距离,但是有些情况下,会让你在求出最短距离的同时还要把最短路径求出(经过哪几个节点),这时候要怎么做呢?
可以采用一个数组pro[V]来保存每个节点的前驱节点。在这里我使用Dijkstra算法为例,在求解最短距离时满足d[j]=d[k]+cost[k][j]时,就说明j的前驱节点是k,那么就记录pro[j]=k;所以只需要在之前的代码中加入一行代码:pro[u]=v;

#include <iostream>
#define MAX_V 1000
#define INF 100000000000

using namespace std;

int cost[MAX_V][MAX_V]; //存储边的权值
int d[MAX_V];//存储最短距离
bool used[MAX_V];//标记该点是否已经用过,使用过表示该点已计算出最短距离
int V; //顶点数量

int pro[V];//×××××××××××××新加入用来存储前驱节点

void dijkstra(int s){
    fill(d,d+V,INF);
    fill(used,used+V;false);
    d[s]=0;

    while(true){
        int v=-1;
        for(int u=0;u<V;u++){ //从未使用的顶点中选择一个距离最短的顶点
            if(! used[u] && (v== -1 || d[u]<d[v])) v=u;
        }

        if(v==-1) break; //v==-1说明所以的顶点都已经计算完成了

        used[v]=true;    //将v标记为以访问过

        for(int u=0;u<V;u++){  //更新与v相连的所以节点
            d[u]=min(d[u],d[v]+cost[v][u]);
            //*************************************
            pro[u]=v;  //**************在原有的Dijkstra算法中加入该行代码
            //*************************************
        }
    }
}


//取出最短路径
vector<int> getPath(int t) {
    vector<int> path;
    while (t != -1) {
        path.push_back(pro[t]);
        t = pro[t];
    }
    //path中保存的为倒序,现在给path倒过来
    reverse(path.begin(), path.end());
    return path;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值