最短路问题(四种算法与路径还原算法)

1、Bellman-Ford算法:

用Bellman-Ford算法求解单源最短路径问题,单源最短路径是指固定一个起点,求它到其他所有点的最短路问题。

struct edge
{
    int from, to, cost;  //从顶点from指向顶点to的权值为cost的边
};
edge es[MAX_E];  //边
int d[MAX_V];    //到出发点的最短距离
int V, E;        //V是顶点数,E是边数

//求解从顶点s出发到所有点的最短距离
void shortest_path(int s)
{
    for(int i = 0; i < V; i++) d[i] = INF;
    d[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.from] != INF 是判断e.from这一点是否之前更新过,如果不加也行,因为如果
             //  d[e.from] == INF 则d[e.to] > d[e.from] + e.cost也一定不会成立,如果
             //  d[e.to] > d[e.from] + e.cost成立,那么则需要更新节点
                d[e.to] = d[e.from] + e.cost;
                update = true;  //判断是否更新结束
            }
        }
        if(!update) break; //判断是否更新结束
    }
}


该算法是以边为基础的,如果在图中不存在从起点可达的负圈(负圈的意思就是,有路径上的权值为负数,一旦有负数存在,那么会不断的迭代更新,因为有负数所有权值可以一直减小。)那么最短路不会经过同一个顶点两次(也就是说,最多通过|V|-1条边),while(true)循环最多执行|V| - 1次,因此复杂度为o(V·E)。

反之,如果存在从s可达的负圈,那么在第|V|次循环中也会更新d的值,因此可以利用这个性质来检查负圈。代码:
 

//如果返回True则存在负圈
bool find_negative_loop()
{
    memset(d, 0, sizeof(d));  
    //把所有初始距离设置为0,检测是否有负圈,
    //其实就是检测一共有几次循环,是否可以在第|V| - 1次循环那里停止。
    for(int i = 1; i <= V; i++){
        for(int j = 0; j < E; j++){
            edge e = es[j];
            if(d[e.from] > d[e.to] + e.cost){
                d[e.from] = d[e.to] + e.cost;
                if(i == V) return true;  //第V次仍然更新了,则存在负圈。
            }
        }
    }
    return false;
}

2、SPFA算法:

SPFA(shortest path fast algorithm),该算法这样命名一看就是谦虚的中国人发现的(外国人一般喜欢用自己的名字命名),言归正转,其实SPFA就是再Bellman-Ford基础上加了一个队列优化,可以求解带负边的但不可以求带负圈的,时间复杂度大家可以认为是0(KE)K<=2 ,所以几乎是线性的复杂度,称为shortest,当之无愧。
其实大家大家发现没有,在Bellman-Ford该算法中,每次都要把所有的边遍历一遍,但其实只需要遍历与可以松弛的点相连的边就行,这里用vector实现邻接链表写下SPFA模板,大家一看就应该可以看懂,个人感觉链式前向星麻烦点。

#include
#include
#include
#include
#include
#include
#define MAX_V 10000
#define INF 0x3f3f3f3f
using namespace std;

typedef pair P;  // p.first 存的为指向的点  p.second 存的是cost
vector

G[MAX_V]; int vis[MAX_V]; int d[MAX_V]; int n; void spfa(int s) { queue que; //初始化一个队列 保存点 fill(d, d + 1 + n, INF); fill(vis, vis + 1 + n, 0); que.push(s); d[s] = 0; vis[s] = 1; while(!que.empty()){ //当队列不为空的时候 == 》 还有可以松弛的边 int v = que.front(); que.pop(); vis[v] = 0; for(int i = 0; i < G[v].size(); i++){ //更新与该点相连的点的离s的距离 int temp_v = G[v][i].first; if(d[temp_v] > d[v] + G[v][i].second){ //如果说该点可以松弛,则进行松弛操作 d[temp_v] = d[v] + G[v][i].second; if(vis[temp_v] == 0){ //如果说该点已经被放到队列里面,那么不用再更新了 que.push(temp_v); vis[temp_v] = 1; //标记为已经进入队列,以防止重复进入,其实重复进入也没事,就是增加了时间复杂度,因为可能需要进行一些无畏的计算 } } } } } int main() { int m; cin>>n>>m; for(int i = 1; i <= m; i++){ int x, y, cost; scanf("%d %d %d", &x, &y, &cost); G[x].push_back(P(y, cost)); //利用vector当邻接链表 G[y].push_back(P(x, cost)); //若为有向图去掉该行 } spfa(1); printf("%d\n", d[n]); }

2、Dijkstra算法:

主要对点进行操作,使用的是邻接矩阵实现的,时间复杂度为o(|V|^2)。

最短路问题(四种算法与路径还原算法)_第1张图片

最短路问题(四种算法与路径还原算法)_第2张图片

int cost[MAX_V][MAX_V];  //cost[u][v]表示边e=(u, v)的权值(不存在这条边时设为INF)
int d[MAX_V];            //从顶点s出发的最短距离
bool used[MAX_V];        //已经使用过的图中的点
int V;                   //顶点数
//从s出发到各个顶点的距离
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; //没有可跟新的了,结束
        
        used[v] = true;
        
        for(int u = 0; u < V; u++){
            d[u] = min(d[u], d[v] + cost[v][u]);  //因为加入了一个点V所以所有的d都要再更新一遍
        } 
    }
}

优化:用STL里面的priority_queue实现:

需要优化的是数值的插入(更新)和取出最小值,两个操作,所以可以用堆来维护每个顶点当前最短距离。在更新最短距离的时候,把对应的元素往根的方向上移动以满足堆的性质,而每次从堆中取出的最小值就是下次使用的顶点,这样堆中元素一共o(V)个,更新和取出的操作有o(E)次,因此整个算法的时间复杂度为:o(E · logV). 所以下面将用优点队列来实现。

优先队列知识补充:
priority_queue
Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。
如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator<,也就是优先队列是大顶堆,队头元素最大。
 

struct edge
{
    int to, cost;
};

typedef pair P;
//因为pair定义了自己的排序规则,即先按照第一维,当第一维相同时,才比较第二维;
//所以这里定义的P 的 first 代表是最短距离 second 代表是顶点的编号;
int V;  //顶点数
vector G[MAX_V]; //
int d[MAX_V];

void dijkstra(int s)
{
    priority_queue, greater

> que; fill(d, d + V, INF); d[s] = 0; que.push(P(0, s)); while(!empty(que)){ P p = que.top(); //取堆顶元素 que.pop(); int v = p.second; if(d[v] < p.first) continue; //当取出的最小值不是最短距离,则丢弃该值 for(int i = 0; i < G[v].size; i++){ //把与v相连的所有的顶点都跟新一遍 edge e = G[v][i]; if(d[e.to] > d[v] + e.cost){ d[e.to] = d[v] + e.cost; que.push_back(P(d[e.to], e.to)); } } } }

相对于Bellman-Ford的o(|V||E|)的复杂度,Dijkstra算法的复杂度是o(|E|log|V|),可以更高效的计算最短路的长度。但是,如果说图中存在负边的情况下,Dijkstra算法就无法正确求解问题,还是需要Bellman-Ford算法。

3、Floyd-Warshall算法:

int d[MAX_V][MAX_V];
int V;

void waeshall_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]);
            }
        }
    }
}

直接遍历,求解各个点之间的最短路径,简单易懂。它还可以处理边是负数的情况。判断图中是否存在负圈,只需要检查是否存在d[ i ][ i ]是负数的顶点 i 就够了。

路径还原:

前三个算法都是求解最终距离的,这里说一下,如果需要输出路径怎么办。
可以用一个prev[ j ]来记录最短路上顶点 j 的前驱,那么在o(|V|)的时间内完成最短路径的恢复。在d[ j ]被d[ j ] = d[ k ] + cost[ k ][ j ]更新时,修改prev[ j ] = k,这样就可以求出来prev的数组,可以在以上算法中用路径前驱标记法来还原出来路径。
这里给出dijkstra算法的路径还原算法代码:

int cost[MAX_V][MAX_V];  //cost[u][v]表示边e=(u, v)的权值(不存在这条边时设为INF)
int d[MAX_V];            //从顶点s出发的最短距离
bool used[MAX_V];        //已经使用过的图中的点
int V;                   //顶点数
int prev[MAX_V];         //记录前驱点
//从s出发到各个顶点的距离
void dijkstra(int s)
{
    fill(d, d + V, INF);            //初始化
    fill(used, used + V, false);    //初始化
    fill(prev, prev + V, -1);
    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; //没有可跟新的了,结束

        used[v] = true;

        for(int u = 0; u < V; u++){
            d[u] = min(d[u], d[v] + cost[u][v]);  //因为加入了一个点V所以所有的d都要再更新一遍
            prev[u] = v;
        }
    }
}

vector get_path(int t) //到顶点t的最短路
{
    vector path;
    for(; t != -1; t = prev[t])
        path.push_back(t);
    reverse(path.begin(), path.end());
    return path;
} 

 

你可能感兴趣的:(ACM,图论)