最短路之單源最短路

        在學習圖論的過程中,最短論問題是比較常見且又具有代表性的一類問題。最短路是給定兩個定點,在以這兩個點作爲起點和終點的路徑中,邊的權值和最小的路徑。在實際生活中,最常見的最短路問題,就是在地圖導航上應用。比如我們把權值作爲距離,那麼我們就可以求得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 edge{
    int to, cost;
};
typedef pair<int, int> P;
vector<edge> graph[MAXN];
int mindist[MAXN];

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

int Dijkstra_heap(int s, int t, int n){
    //pair的first存放s->v的距離
    //second存放頂點v
    priority_queue<P, vector<P>, greater<P> > Q;
    //初始化源點s的信息
    mindist[s] = 0;
    Q.push(P(0, s));
    while(!Q.empty()){
        //每次從堆中取出最小值
        P p = Q.top(); Q.pop();
        int v = p.second;
        //當取出的值不是當前最短距離的話,就丟棄這個值
        if(mindist[v] < p.first) continue;
        //將與其相鄰的點,進行鬆弛操作
        for(unsigned int i = 0; i < graph[v].size(); i++){
            edge e = graph[v][i];
            if(mindist[e.to] > mindist[v] + e.cost){
                mindist[e.to] = mindist[v] + e.cost;
                //將滿足條件的點重新加入堆中
                Q.push(P(mindist[e.to], e.to));
            }
        }
    }
    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({v2, x});
            graph[v2].push_back({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題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章