最短路之单源最短路

        在学习图论的过程中,最短论问题是比较常见且又具有代表性的一类问题。最短路是给定两个定点,在以这两个点作为起点和终点的路径中,边的权值和最小的路径。在实际生活中,最常见的最短路问题,就是在地图导航上应用。比如我们把权值作为距离,那么我们就可以求得A到B的最短路径。如果时间作为权值,那么我们就可以得到A到B的最短时间。

1、Bellman-Ford算法

         单源最短路问题就是将起点固定,求该起点到其他所有点的最短路问题。贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特(Lester Ford) 创立的,求解单源最短路径问题的一种算法。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行|V|-1次松弛操作,得到所有可能的最短路径。其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。

①算法描述

        设dist[v]表示从源点s到v的最短路径长度。对于与v相邻的任意顶点u,dist[v]满足三角不等式:

                 dist[v] ≤ dist[u] + w(u, v),   (其中w(u,v)为边(u, v)的权值)

        我们设d[v]为s到v的最短路权值上界(可能为无穷大,即不连通),称为最短路估计。

        如果 d[v] > d[u] + w(u, v),即说明d[v]还可以变得更小。于是我们就使 d[v] = d[u] + w(u, v),我们称这个操作为松弛操作。 

        显然每次通过松弛操作我们都可以使得d[v]减小,直到d[v]的值不在变化(即当d[v]等于dist[v])。

我们容易知道,在一个没有负权环的图中。每一个顶点至多与其它|V|-1个顶点进行松弛操作,若大于|V|-1,则必然存在负权环。

对于图G(V,E),下面给出Bellman-Ford的算法流程

输入:图G和起点S
输出:s到每一个点的最短路径,以及图中是否存在负权环
具体流程:
1、初始化d数组,d[s] = 0, d[i] = ∞ (i ≠ s)
2、枚举每一条边,进行松弛操作
3、将操作2重复执行|V|-1次
4、枚举每一条边,看是否能够进行松弛操作,若能,这说明原图存在负权环

②时间复杂度

对于Bellman-Ford,由于每次操作需要枚举|E|条边,总共需要重复|V|-1次操作,则我们容易得出其时间复杂度为O(VE)。如果我们使用队列进行优化,则时间复杂度可下降为O(kE),k是个比较小的系数(并且在绝大多数的图中,k<=2,然而在一些精心构造的图中可能会上升到很高)。

③代码实现

我们以 hduXYZZY为例:

<1>未优化版

#include <cstdio>
#include <cstring>
#define INF 0xfffffff
#define MAXN (100 + 10)
using namespace std;

struct edge{
    int from, to;
    edge(int f = 0, int t = 0)
    : from(f), to(t){}
};

edge es[MAXN*MAXN];
int cost[MAXN];
bool graph[MAXN][MAXN];
int d[MAXN];
//判断图是否联通
void Floyd(int n){
    for(int i = 1; i <= n; i++){
        for(int k = 1; k <= n; k++){
            for(int j = 1; j <= n; j++){
                if(!graph[i][j])
                    graph[i][j] = graph[i][k] && graph[k][j];
            }
        }
    }
}

bool bellman_ford(int s, int V, int E){
    for(int i = 0; i <= V; i++)
        d[i] = -INF;
    d[s] = 100;
    //重复对每一条边进行松弛操作
    for(int k = 0; k < V-1; k++){
        for(int i = 0; i < E; i++){
            edge e = es[i];
            //松弛操作
            if(d[e.to] < d[e.from] + cost[e.to] && d[e.from] + cost[e.to] > 0){
                d[e.to] = d[e.from] + cost[e.to];
            }
        }
    }
    //检查负权环
    for(int i = 0; i < E; i++){
        edge e = es[i];
        if(d[e.to] < d[e.from] + cost[e.to] && graph[e.to][V] && d[e.from] + cost[e.to] > 0)
            return true;
    }
    return d[V] > 0;
}

int main(){
    int n, m, cnt, vex;
    while(scanf("%d", &n), n != -1){
        memset(graph, false, sizeof(graph));
        cnt = 0;
        for(int i = 1; i <= n; i++){
            scanf("%d%d", &cost[i], &m);
            for(int j = 0; j < m; j++){
                scanf("%d", &vex);
                es[cnt++] = edge(i, vex);
                graph[i][vex] = true;
            }
        }
        Floyd(n);
        if(!graph[1][n] || !bellman_ford(1, n, cnt)){
            printf("hopeless\n");
        }
        else{
            printf("winnable\n");
        }
    }
    return 0;
}

<2>队列优化SPFA

单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm。 SPFA算法是西南交通大学段凡丁于1994年发表的。松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算。我们还是以hdu1317xyzzy为例,代码如下:

#include <cstdio>
#include <cstring>
#include <queue>
#define MAXN (100 + 10)
using namespace std;
//d表示s到各点的所经过路径的权值之和
//cost表示各点的权值
//cnt表示进入队列的次数
int d[MAXN], cost[MAXN], cnt[MAXN];
//reach表示两点之间是否联通,即可达
//graph记录两点之间是否有边
bool reach[MAXN][MAXN], graph[MAXN][MAXN];


void Init(){
    memset(d, 0, sizeof(d));
    memset(cnt, 0, sizeof(cnt));
    memset(graph, false, sizeof(graph));
    memset(reach, false, sizeof(reach));
}

//判断图是否联通
void Floyd(int n){
    for(int i = 1; i <= n; i++){
        for(int k = 1; k <= n; k++){
            for(int j = 1; j <= n; j++){
                if(!reach[i][j])
                    reach[i][j] = reach[i][k] && reach[k][j];
            }
        }
    }
}

bool SPFA(int s, int n){
    queue<int> Q;
    d[s] = 100;
    Q.push(s);
    while(!Q.empty()){
        int now = Q.front();
        Q.pop();
        cnt[now]++;
        //如果不存在负权环(PS:在本题中为正权环),即每个点进入队列的次数至多为n-1
        //若大于n-1,即表明必然存在负权环
        if(cnt[now] >= n) return reach[now][n];
        //依次枚举每条边
        for(int next = 1; next <= n; next++){
            if(graph[now][next] && d[now] + cost[next] > d[next] && d[now] + cost[next] > 0){
                Q.push(next);
                d[next] = d[now] + cost[next];
            }
        }
    }
    return d[n] > 0;
}

int main(){
    int n, m, vex;
    while(scanf("%d", &n), n != -1){
        Init();
        for(int i = 1; i <= n; i++){
            scanf("%d%d", &cost[i], &m);
            for(int j = 0; j < m; j++){
                scanf("%d", &vex);
                reach[i][vex] = true;
                graph[i][vex] = true;
            }
        }
        Floyd(n);
        if(!reach[1][n] || !SPFA(1, n)){
            printf("hopeless\n");
        }
        else{
            printf("winnable\n");
        }
    }
    return 0;
}


2、Dijkstra算法

我们容易发现,如果图中没有负边的情况。在Bellman-Ford算法中,如果d[u]还不是最短距离的话,那么即便我们进行了松弛操作,那么d[v]也不会变为最短距离。而且即便d[v]没有变化,那么他还是需要检查一次所有的边。显然这些操作很浪费时间,于是乎,我们就提出了以下改进:

(1)从最短距离已经确定的顶点出发更新与之相邻顶点的最短距离。

(2)对于最短距离已经确定的顶点,我们直接无视。

通过这样的修改我们就得到了Dijkstra算法。Dijkstra算法是用来解决只含非负权值边的图的单源最短路问题。换而言之,Dijkstra无法处理含有负权边的图。

①算法描述

对于图G(V,E),下面给出Dijkstra的算法流程

输入:图G和起点S
输出:s到每一个点的最短路径
具体流程:
1、初始化d数组,d[s] = 0, d[i] = ∞ (i ≠ s)
2、设置所有点未访问过(即设置一个标记数组,并将其置空)
3、找出所有未访问过的点中距离值最小的点,将其标记为访问过
4、对3中找到的点的相邻边进行松弛操作
5、重复3和4直到所有点都访问过

下边给出一幅图来模拟Dijkstra的过程:



②时间复杂度

因为操作更新|V|次,每次操作需要找最小值,扫描一个点连接的所有边,如果我们使用堆来实现寻找和维护,则时间复杂度为O( (|E|+|V|) log|V| )。若只用普通的方法扫描,时间复杂度为O(|V|² + |E|)。

③代码实现

我们以hdu 1874畅通工程续 来说明Dijkstra算法:

<1>未优化版

#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 200 + 10
#define INF 0xffffff
using namespace std;
struct Vex{
    int v, weight;
    Vex(int tv, int tw):v(tv), weight(tw){}
};
//graph用来记录图的信息
vector<Vex> graph[MAXN];
//判断是否已经找到最短路
bool inTree[MAXN];
//源点s到各顶点最短路的值
int mindist[MAXN];
//初始化
void Init(int n){
    for(int i = 0; i < n; i++){
        inTree[i] = false;
        graph[i].clear();
        mindist[i] = INF;
    }
}
//s表示源点,t表示终点,n表示顶点数目
int Dijkstra(int s, int t, int n){
    int tempMin, tempVex, addNode;
    //初始化s
    mindist[s] = 0;
    //将源点s标记为访问过
    inTree[s] = true;
    //题目中可能有重边,我们去除重边
    for(unsigned int i = 0; i < graph[s].size(); i++)
        mindist[graph[s][i].v] = min(mindist[graph[s][i].v], graph[s][i].weight);
    //从剩下的n-1个点逐个枚举
    for(int nNode = 1; nNode <= n-1; nNode++){
        tempMin = INF;
        //寻找所有未访问过点中,有最小距离的点
        for(int i = 0; i < n; i++){
            if(!inTree[i] && mindist[i] < tempMin){
                tempMin = mindist[i];
                addNode = i;
            }
        }
        //将该点标记为访问过
        inTree[addNode] = true;
        //将与该点相邻的点进行松弛操作
        for(unsigned int i = 0; i < graph[addNode].size(); i++){
            tempVex = graph[addNode][i].v;
            if(!inTree[tempVex] && tempMin + graph[addNode][i].weight < mindist[tempVex]){
                mindist[tempVex] = tempMin + graph[addNode][i].weight;
            }
        }
    }
    return mindist[t];
}


int main(){
    int n, m;
    int v1, v2, x, s, t;
    while(scanf("%d%d", &n, &m) != EOF){
        Init(n);
        for(int i = 0; i < m; i++){
            scanf("%d%d%d", &v1, &v2, &x);
            graph[v1].push_back(Vex(v2, x));
            graph[v2].push_back(Vex(v1, x));
        }
        scanf("%d%d", &s, &t);
        int ans = Dijkstra(s, t, n);
        if(ans == INF)
            printf("-1\n");
        else
            printf("%d\n", ans);
    }
    return 0;
}

<2>堆优化版

#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
#define MAXN 200 + 10
#define INF 0xffffff
using namespace std;
struct Vex{
    int v, weight;
    bool operator < (const Vex & t) const{
        return this->weight > t.weight;
    }
    Vex(int tv = 0, int tw = 0):v(tv), weight(tw){}
};
vector<Vex> graph[MAXN];
bool inTree[MAXN];
int mindist[MAXN];

void Init(int n){
    for(int i = 0; i < n; i++){
        inTree[i] = false;
        graph[i].clear();
        mindist[i] = INF;
    }
}

int Dijkstra_heap(int s, int t, int n){
    priority_queue<Vex> Q;
    Vex tempVex;
    int v1, v2, weight;
    //初始化源点s的信息
    mindist[s] = 0;
    Q.push(Vex(s, 0));
    while(!Q.empty()){
        //每次从堆中取出最小值
        tempVex = Q.top();
        Q.pop();
        v1= tempVex.v;
        if(inTree[v1]) continue;
        //如果没有访问过,则我们将其标记为访问过
        inTree[v1] = true;
        //将与其相邻的点,进行松弛操作
        for(unsigned int i = 0; i < graph[v1].size(); i++){
            v2 = graph[v1][i].v;
            weight = graph[v1][i].weight;
            if(!inTree[v2] && mindist[v1] + weight < mindist[v2]){
                mindist[v2] = mindist[v1] + weight;
                //将满足条件的点重新加入堆中
                Q.push(Vex(v2, mindist[v2]));
            }
        }
    }
    return mindist[t];
}


int main()
{
    int n, m;
    int v1, v2, x, s, t;
    while(scanf("%d%d", &n, &m) != EOF){
        Init(n);
        for(int i = 0; i < m; i++){
            scanf("%d%d%d", &v1, &v2, &x);
            graph[v1].push_back(Vex(v2, x));
            graph[v2].push_back(Vex(v1, x));
        }
        scanf("%d%d", &s, &t);
        int ans = Dijkstra_heap(s, t, n);
        if(ans == INF)
            printf("-1\n");
        else
            printf("%d\n", ans);
    }
    return 0;
}

PS:如果不懂优先队列的,请移步: 传送门

PPS:在附赠一个大礼包,图论500题

你可能感兴趣的:(图论,最短路,dijkstra,SPFA,Bellman-Ford)