貪心算法-最短路徑


參考作者:Vamei 出處:http://www.cnblogs.com/vamei

圖是由節點和連接節點的邊構成的。節點之間可以由路徑,即邊的序列。根據路徑,可以從一點到達另一點。在一個復雜的圖中,圖中兩點可以存在許多路徑。最短路徑討論了一個非常簡單的圖論問題,圖中從A點到B點 ,那條路徑耗費最短?

 

這個問題又異常復雜,因為網絡的構成狀況可能很復雜。

一個最簡單的思路,是找出所有可能的從A到B的路徑,再通過比較,來尋找最短路徑。然而,這並沒有將問題簡化多少。因為搜索從A到B的路徑,這本身就是很復雜的事情。而我們在搜索所有路徑的過程中,有許多路徑已經繞了很遠,完全沒有搜索的必要。比如從上海到紐約的路線,完全沒有必要先從上海飛到南極,再從南極飛到紐約,盡管這一路徑也是一條可行的路徑。

所以,我們需要這樣一個算法:它可以搜索路徑,並當已知路徑包括最短路徑時,即停止搜索。我們先以無權網絡為例,看一個可行的最短路徑算法。

 

無權網絡

無權網絡(unweighted network)是相對於加權網絡的,這里的“權”是權重。每條邊的耗費相同,都為1。路徑的總耗費即為路徑上邊的總數。

 

我們用“甩鞭子”的方式,來尋找最短路徑。鞭子的長度代表路徑的距離。

 

手拿一個特定長度的鞭子,站在A點。甩出鞭子,能打到一些點。比如C和D。

將鞭子的長度增加1。再甩出鞭子。此時,從C或D出發,尋找距離為1的鄰接點,即E和F。這些點到A點的距離,為此時鞭子的長度。

記錄點E和F,並記錄它們的上游節點。比如E(C), F(D)。我們同樣可以記錄此時該點到A的距離,比如5。

 

 

如果要記錄節點E時,發現它已經出現在之前的記錄中,這說明曾經有更短的距離到E。此時,不將E放入記錄中。畢竟,我們感興趣的是最短路徑。如下圖中的E:

黃色的E不被記錄

 

最初的鞭子長度為0,站在A點,只能打到A點自身。當我們不斷增加鞭子長度,第一次可以打到B時,那么此時鞭子的長度,就是從A到B的最短距離。循着我們的記錄,倒推上游的節點,就可以找出整個最短路徑。

我們的記錄本是個很有意思的東西。某個點放入記錄時,此時的距離,都是A點到該點的最短路徑。根據記錄,我們可以反推出記錄中任何一點的最短路徑。這就好像真誠對待每個人。這能保證,當你遇到真愛時,你已經是在真誠相待了。實際上,記錄將所有節點分割成兩個世界:記錄內的,已知最短距離的;記錄外的,未知的。

 

加權網絡

在加權網絡中(weighted network),每條邊有各自的權重。當我們選擇某個路徑時,總耗費為路徑上所有邊的權重之和。

加權網絡在生活中很常見,比如從北京到上海,可以坐火車,也可以坐飛機。但兩種選擇耗費的時間並不同。再比如,我們打出租車和坐公交車,都可以到市區,但車資也有所不同。在計算機網絡中,由於硬件性能不同,連接的傳輸速度也有所差異。加權網絡正適用於以上場景。無權網絡是加權網絡的一個特例。

 

這個問題看起來和無權網絡頗為類似。但如果套用上面的方法,我們會發現,記錄中的節點並不一定是最短距離。我們看下面的例子:

很明顯,最短路徑是A->C->D->B,因為它的總耗費只有4。按照上面的方法,我們先將A放入記錄。從A出發,有B和C兩個如果將B和C同時放入記錄,那么記錄中的B並不符合最短距離的要求。

那么,為什么無權網絡可行呢?假設某次記錄時,鞭子長度為5,那么這次記錄點的鄰接點,必然是距離為6的點。如果這些鄰接點沒有出現過,那么6就是它們的最短距離。所有第一次出現的鄰接點,都將加入到下次的記錄中。比如下面的例子,C/D/E是到達A的鄰接點,它們到A的最短距離必然都是1。

對於加權網絡來說,即使知道了鄰接點,也無法判斷它們是否符合最短距離。在記錄C/D/E時,我們無法判斷未來是否存在如下圖虛線的連接,導致A的鄰接點E並不是下一步的最短距離點:

 

但情況並沒有我們想的那么糟糕。仔細觀察,我們發現,雖然無法一次判定所有的鄰接點為下一步的最短距離點,但我們可以確定點C已經處在從A出發的最短距離狀態。A到C的其它可能性,比如途徑D和E,必然導致更大的成本。

也就是說,鄰接點中,有一個達到了最短距離點,即鄰接點中,到達A距離最短的點,比如上面的C。我們可以安全的把C改為已知點。A和C都是已知點,點P成為新的鄰接點。P到A得距離為4。

 

出於上面的觀察,我們可以將節點分為三種:

  • 已知點:已知到達A最短距離的點。“我是成功人士。”
  • 鄰接點:有從記錄點出發的邊,直接相鄰的點。“和成功人士接觸,也有成功的機會哦。”
  • 未知點:“還早得很。”

 

最初的已知點只有A。已知點的直接下游節點為鄰接點。對於鄰接點,我們需要獨立的記錄它們。我們要記錄的有:

  • 當前情況下,從A點出發到達該鄰接點的最短距離。比如對於上面的點D,為6。
  • 此最短距離下的上游節點。對於上面的點D來說,為A。

每次,我們將鄰接點中最短距離最小的點X轉為已知點,並將該點的直接下游節點,改為鄰接點。我們需要計算從A出發,經由X,到達這些新增鄰接點的距離:新距離 = X最短距離 + QX邊的權重。此時有兩種情況,

  • 如果下游節點Q還不是鄰接點,那么直接加入,Q最短距離 = 新距離,Q上游節點為X。
  • 如果下游節點Q已經是鄰接點,記錄在冊的上游節點為Y,最短距離為y。如果新距離小於y,那么最小距離改為新距離,上游節點也改為X。否則保持原記錄不變。

 

我們還用上面的圖,探索A到E的路徑:

第一步

  狀態 已知距離 上游
A 已知 0 A
鄰接 1 A
D 鄰接
E 鄰接
P 未知  無窮  

第二步

  狀態 已知距離 上游
A 已知 0 A
已知 1 A
D 鄰接
E 鄰接
P 鄰接 4 C

第二步

  狀態 已知距離 上游
A 已知 0 A
已知 1 A
D 鄰接
E 鄰接 7 P
P 已知  4 C

第三步

  狀態 已知距離 上游
A 已知 0 A
已知 1 A
D 已知
E 鄰接 7 P
P 已知  4 C

最后,E成為已知。倒退,可以知道路徑為E, P, C, A。正過來,就是從A到E的最短路徑了。

 

上面的算法是經典的Dijkstra算法。本質上,每個鄰接點記錄的,是基於已知點的情況下,最好的選擇,也就是所謂的“貪婪算法”(greedy algorithm)。當我們貪婪時,我們的決定是臨時的,並沒有做出最終的決定。轉換某個點成為已知點后,我們增加了新的可能性,貪婪再次起作用。根據對比。隨后,某個鄰接點成為新的“貪無可貪”的點,即經由其它任意鄰接點,到達該點都只會造成更高的成本; 經由未知點到達該點更不可能,因為未知點還沒有開放,必然需要經過現有的鄰接點到達,只會更加繞遠。好吧,該點再也沒有貪婪的動力,就被扔到“成功人士”里,成為已知點。成功學不斷傳染,最后感染到目標節點B,我們就找到了B的最短路徑。

 

實現

理解了上面的原理,算法的實現是小菜一碟。我們借用圖 (graph)中的數據結構,略微修改,構建加權圖。

我們將上面的表格做成數組records,用於記錄路徑探索的信息。

重新給點A,C,D,E,P命名,為0, 1, 2, 3, 4。

 

代碼如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

typedef struct Node {
    int element;
    int weight;
    struct Node *next;
}node;
typedef struct {
    int status;
    int distance;
    int previous;
}record;

void insertEdge(node *graph, int from, int to, int weight);  //構建圖
void printGraph(node *graph, int n);  //打印圖
void shortPath(node *graph, record *records, int n, int from, int to);  //尋找最短路徑
int newNeighbors(node *graph, record *records, int n, int current);  //尋找鄰接點

int main(void)
{
    int n, m;
    int s, e, w;
    node *graph;
    record *records;

    scanf("%d%d", &n, &m);
    graph = (node *)calloc(n, sizeof(node));
    records = (record *)calloc(n, sizeof(record));

    for (int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &s, &e, &w);
        insertEdge(graph, s, e, w);
    }
    printGraph(graph, n);
    printf("\n0 -> 3:\n");
    shortPath(graph, records, n, 0, 3);  //尋找從0到3的最短路徑

    system("pause");
    return 0;
}

void insertEdge(node * graph, int from, int to, int weight)
{
    node *add, *temp;

    temp = graph + from;
    add = (node *)calloc(1, sizeof(node));
    add->element = to;
    add->weight = weight;
    add->next = temp->next;
    temp->next = add;
}

void printGraph(node * graph, int n)
{
    node *temp;

    for (int i = 0; i < n; i++)
    {
        printf("%d :", i);

        temp = (graph + i)->next;
        while (temp)
        {
            printf("%d(%d) ", temp->element, temp->weight);
            temp = temp->next;
        }
        printf("\n");
    }
}

void shortPath(node * graph, record * records, int n, int from, int to)
{
    int current = from;

    for (int i = 0; i < n; i++)  //初始化記錄表
    {
        (records + i)->status = -1;
        (records + i)->distance = INT_MAX;
        (records + i)->previous = -1;
    }

    (records + from)->status = 1;  //起點
    (records + from)->distance = 0;
    (records + from)->previous = 0;
    while (current != to)
    {
        current = newNeighbors(graph, records, n, current);
    }

    while (current != from)
    {
        printf("%d<- ", current);
        current = (records + current)->previous;
    }
    printf("%d\n", current);
}

int newNeighbors(node * graph, record * records, int n, int current)
{
    node *temp = (graph + current)->next;
    int element, distance;

    while (temp)  //探索鄰接點
    {
        element = temp->element;
        (records + element)->status = 0;
        distance = temp->weight + (records + current)->distance;
        if ((records + element)->distance > distance)
        {
            (records + element)->distance = distance;
            (records + element)->previous = current;
        }
        temp = temp->next;
    }

    int d = INT_MAX;
    for (int i = 0; i < n; i++)  //下一個已知點
    {
        if ((records + i)->status == 0 && (records + i)->distance < d)
        {
            d = (records + i)->distance;
            element = i;
        }
    }
    (records + element)->status = 1;

    return element;
}

 運行結果如下:

5 5
0 1 1
0 2 6
0 3 9
1 4 3
4 3 3
0 :3(9) 2(6) 1(1)
1 :4(3)
2 :
3 :
4 :3(3)

0 -> 3:
3<- 4<- 1<- 0

上面的算法中,最壞情況是目標節點最后成為已知點,即要尋找O(|V|)O(|V|)。而每個已知點是通過尋找O(|V|)O(|V|)個節點的最小值得到的。最后,打印出最短的路徑過程中,需要倒退,最多可能有O|E|O|E|,也就是說,算法復雜度為O(|V|2+|E|)O(|V|2+|E|)。

上面的records為一個數組,用於記錄路徑探索信息。我們可以用一個優先隊列來代替它,將已知的節點移除優先隊列。這樣可以達到更好的運算效率。

總結

最短路徑是尋找最優解的算法。在復雜的網絡中,簡單的實現方式無法運行,必須求助於精心設計的算法,比如這里的Dijkstra算法。利用貪婪的思想,我們不斷的優化結果,直到找到最優解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM