日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

最短路徑問題

 收藏小管 2017-04-24

只想說:溫故而知新,可以為師矣。我大二的《數(shù)據(jù)結(jié)構(gòu)》是由申老師講的,那時候不怎么明白,估計太理論化了(ps:或許是因為我睡覺了);今天把老王的2011年課件又看了一遍,給大二的孩子們又講了一遍,隨手谷歌了N多資料,算是徹底搞懂了最短路徑問題。請讀者盡情享用……

        我堅信:沒有不好的學(xué)生,只有垃圾的教育。不過沒有人理所當(dāng)然的對你好,所以要學(xué)會感恩。

一.問題引入

        問題:從某頂點出發(fā),沿圖的邊到達另一頂點所經(jīng)過的路徑中,各邊上權(quán)值之和最小的一條路徑——最短路徑。解決最短路的問題有以下算法,Dijkstra算法,Bellman-Ford算法,F(xiàn)loyd算法和SPFA算法,另外還有著名的啟發(fā)式搜索算法A*,不過A*準備單獨出一篇,其中Floyd算法可以求解任意兩點間的最短路徑的長度。筆者認為任意一個最短路算法都是基于這樣一個事實:從任意節(jié)點A到任意節(jié)點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A經(jīng)過若干個節(jié)點到B。

二.Dijkstra算法

        該算法在《數(shù)據(jù)結(jié)構(gòu)》課本里是以貪心的形式講解的,不過在《運籌學(xué)》教材里被編排在動態(tài)規(guī)劃章節(jié),建議讀者兩篇都看看。

           image

        觀察右邊表格發(fā)現(xiàn)除最后一個節(jié)點外其他均已經(jīng)求出最短路徑。

        (1)   迪杰斯特拉(Dijkstra)算法按路徑長度(看下面表格的最后一行,就是next點)遞增次序產(chǎn)生最短路徑。先把V分成兩組:

  • S:已求出最短路徑的頂點的集合
  • V-S=T:尚未確定最短路徑的頂點集合

        將T中頂點按最短路徑遞增的次序加入到S中,依據(jù):可以證明V0到T中頂點Vk的最短路徑,或是從V0到Vk的直接路徑的權(quán)值或是從V0經(jīng)S中頂點到Vk的路徑權(quán)值之和(反證法可證,說實話,真不明白哦)。

        (2)   求最短路徑步驟

  1. 初使時令 S={V0},T={其余頂點},T中頂點對應(yīng)的距離值, 若存在,為弧上的權(quán)值(和SPFA初始化方式不同),若不存在,為Inf。
  2. 從T中選取一個其距離值為最小的頂點W(貪心體現(xiàn)在此處),加入S(注意不是直接從S集合中選取,理解這個對于理解vis數(shù)組的作用至關(guān)重要),對T中頂點的距離值進行修改:若加進W作中間頂點,從V0到Vi的距離值比不加W的路徑要短,則修改此距離值(上面兩個并列for循環(huán),使用最小點更新)。
  3. 重復(fù)上述步驟,直到S中包含所有頂點,即S=V為止(說明最外層是除起點外的遍歷)。

        下面是上圖的求解過程,按列來看,第一列是初始化過程,最后一行是每次求得的next點。

           image

        (3)   問題:Dijkstar能否處理負權(quán)邊?(下面的解釋引自網(wǎng)上某大蝦)

             答案是不能,這與貪心選擇性質(zhì)有關(guān)(ps:貌似還是動態(tài)規(guī)劃啊,暈了),每次都找一個距源點最近的點(dmin),然后將該距離定為這個點到源點的最短路徑;但如果存在負權(quán)邊,那就有可能先通過并不是距源點最近的一個次優(yōu)點(dmin'),再通過這個負權(quán)邊L(L<><>
0,3,4 
3,0,-2 
4,-2,0,用dijkstra求得d[1,2]=3,事實上d[1,2]=2,就是通過了1-3-2使得路徑減小。不知道講得清楚不清楚。

二.Floyd算法

        參考了南陽理工牛帥(目前在新浪)的博客。

        Floyd算法的基本思想如下:從任意節(jié)點A到任意節(jié)點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A經(jīng)過若干個節(jié)點到B,所以,我們假設(shè)dist(AB)為節(jié)點A到節(jié)點B的最短路徑的距離,對于每一個節(jié)點K,我們檢查dist(AK) + dist(KB) < dist(ab)是否成立,如果成立,證明從a到k再到b的路徑比a直接到b的路徑短,我們便設(shè)置="" dist(ab)="dist(AK)" +="">

        很簡單吧,代碼看起來可能像下面這樣:

for (int i=0; i
for (int j=0; j
for (int k=0; k
if (dist[i][k] + dist[k][j] < dist[i][j]="" )="">
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}

        但是這里我們要注意循環(huán)的嵌套順序,如果把檢查所有節(jié)點K放在最內(nèi)層,那么結(jié)果將是不正確的,為什么呢?因為這樣便過早的把i到j(luò)的最短路徑確定下來了,而當(dāng)后面存在更短的路徑時,已經(jīng)不再會更新了。

        讓我們來看一個例子,看下圖:

image

        圖中紅色的數(shù)字代表邊的權(quán)重。如果我們在最內(nèi)層檢查所有節(jié)點K,那么對于A->B,我們只能發(fā)現(xiàn)一條路徑,就是A->B,路徑距離為9,而這顯然是不正確的,真實的最短路徑是A->D->C->B,路徑距離為6。造成錯誤的原因就是我們把檢查所有節(jié)點K放在最內(nèi)層,造成過早的把A到B的最短路徑確定下來了,當(dāng)確定A->B的最短路徑時dist(AC)尚未被計算。所以,我們需要改寫循環(huán)順序,如下:

        ps:個人覺得,這和銀行家算法判斷安全狀態(tài)(每種資源去測試所有線程),樹狀數(shù)組更新(更新所有相關(guān)項)一樣的思想。

for (int k=0; k
for (int i=0; i
for (int j=0; j
/*
實際中為防止溢出,往往需要選判斷 dist[i][k]和dist[k][j
都不是Inf ,只要一個是Inf,那么就肯定不必更新。
*/
if (dist[i][k] + dist[k][j] < dist[i][j]="" )="">
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}

        如果還是看不懂,那就用草稿紙模擬一遍,之后你就會豁然開朗。半個小時足矣(早知道的話會節(jié)省很多個半小時了。。狡猾

       再來看路徑保存問題:

void floyd() {
for(int i=1; i<=n ;="">
for(int j=1; j<= n;="">
if(map[i][j]==Inf){
path[i][j] = -1;//表示 i -> j 不通
}else{
path[i][j] = i;// 表示 i -> j 前驅(qū)為 i
}
}
}
for(int k=1; k<=n; k++)="">
for(int i=1; i<=n; i++)="">
for(int j=1; j<=n; j++)="">
if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
path[i][k] = i;
path[i][j] = path[k][j];
}
}
}
}
}
void printPath(int from, int to) {
/*
* 這是倒序輸出,若想正序可放入棧中,然后輸出。
*
* 這樣的輸出為什么正確呢?個人認為用到了最優(yōu)子結(jié)構(gòu)性質(zhì),
* 即最短路徑的子路徑仍然是最短路徑
*/
while(path[from][to]!=from) {
System.out.print(path[from][to] +' ');
to = path[from][to];
}
}

        《數(shù)據(jù)結(jié)構(gòu)》課本上的那種方式我現(xiàn)在還是不想看,看著不舒服……

        Floyd算法另一種理解DP,為理論愛好者準備的,上面這個形式的算法其實是Floyd算法的精簡版,而真正的Floyd算法是一種基于DP(Dynamic Programming)的最短路徑算法。設(shè)圖G中n 個頂點的編號為1到n。令c [i, j, k]表示從i 到j(luò) 的最短路徑的長度,其中k 表示該路徑中的最大頂點,也就是說c[i,j,k]這條最短路徑所通過的中間頂點最大不超過k。因此,如果G中包含邊,則c[i, j, 0] =邊 的長度;若i= j ,則c[i,j,0]=0;如果G中不包含邊,則c (i, j, 0)= +∞。c[i, j, n] 則是從i 到j(luò) 的最短路徑的長度。對于任意的k>0,通過分析可以得到:中間頂點不超過k 的i 到j(luò) 的最短路徑有兩種可能:該路徑含或不含中間頂點k。若不含,則該路徑長度應(yīng)為c[i, j, k-1],否則長度為 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取兩者中的最小值。狀態(tài)轉(zhuǎn)移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。這樣,問題便具有了最優(yōu)子結(jié)構(gòu)性質(zhì),可以用動態(tài)規(guī)劃方法來求解。

        看另一個DP(直接引用王老師課件)

                       image

        說了這么多,相信讀者已經(jīng)躍躍欲試了,咱們看一道例題,以ZOJ 1092為例:給你一組國家和國家間的部分貨幣匯率兌換表,問你是否存在一種方式,從一種貨幣出發(fā),經(jīng)過一系列的貨幣兌換,最后返回該貨幣時大于出發(fā)時的數(shù)值(ps:這就是所謂的投機倒把吧),下面是一組輸入。 
3    //國家數(shù) 
USDollar  //國家名 
BritishPound 
FrenchFranc 
   3    //貨幣兌換數(shù) 
USDollar 0.5 BritishPound  //部分貨幣匯率兌換表 
BritishPound 10.0 FrenchFranc 
FrenchFranc 0.21 USDollar

        月賽做的題,不過當(dāng)時用的思路是求強連通分量(ps:明明說的,那時我和華杰感覺好有道理),也沒做出來,現(xiàn)在知道了直接floyd算法就ok了。

        思路分析:輸入的時候可以采用Map map = new HashMap()主要是為了獲得再次包含匯率輸入時候的下標以建圖(感覺自己寫的好拗口),或者第一次直接存入String數(shù)組str,再次輸入的時候每次遍歷str數(shù)組,若是equals那么就把str的下標賦值給該幣種建圖。下面就是floyd算法啦,初始化其它點為-1,對角線為1,采用乘法更新求最大值。

三.Bellman-Ford算法

        為了能夠求解邊上帶有負值的單源最短路徑問題,Bellman(貝爾曼,動態(tài)規(guī)劃提出者)和Ford(福特)提出了從源點逐次繞過其他頂點,以縮短到達終點的最短路徑長度的方法。 Bellman-ford算法是求含負權(quán)圖的單源最短路徑算法,效率很低,但代碼很容易寫。即進行不停地松弛,每次松弛把每條邊都更新一下,若n-1次松弛后還能更新,則說明圖中有負環(huán),無法得出結(jié)果,否則就成功完成。Bellman-ford算法有一個小優(yōu)化:每次松弛先設(shè)一個flag,初值為FALSE,若有邊更新則賦值為TRUE,最終如果還是FALSE則直接成功退出。Bellman-ford算法浪費了許多時間做無必要的松弛,所以SPFA算法用隊列進行了優(yōu)化,效果十分顯著,高效難以想象。SPFA還有SLF,LLL,滾動數(shù)組等優(yōu)化。

        關(guān)于SPFA,請看我這一篇http://www.cnblogs.com/hxsyl/p/3248391.html

        遞推公式(求頂點u到源點v的最短路徑):

         dist 1 [u] = Edge[v][u]

         dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u

         Dijkstra算法和Bellman算法思想有很大的區(qū)別:Dijkstra算法在求解過程中,源點到集合S內(nèi)各頂點的最短路徑一旦求出,則之后不變了,修改  的僅僅是源點到T集合中各頂點的最短路徑長度。Bellman算法在求解過程中,每次循環(huán)都要修改所有頂點的dist[ ],也就是說源點到各頂點最短路徑長度一直要到Bellman算法結(jié)束才確定下來。

        算法適用條件

  • 1.單源最短路徑(從源點s到其它所有頂點v)
  • 有向圖&無向圖(無向圖可以看作(u,v),(v,u)同屬于邊集E的有向圖)
  • 邊權(quán)可正可負(如有負權(quán)回路輸出錯誤提示)
  • 差分約束系統(tǒng)(至今貌似只看過一道題)

        Bellman-Ford算法描述:

  1. 初始化:將除源點外的所有頂點的最短距離估計值 d[v] ←+∞, d[s] ←0
  2. 迭代求解:反復(fù)對邊集E中的每條邊進行松弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(運行|v|-1次,看下面的描述性證明(當(dāng)做樹))
  3. 檢驗負權(quán)回路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則算法返回false,表明問題無解;否則算法返回true,并且從源點可達的頂點v的最短距離保存在d[v]中

        描述性證明:(這個解釋很好)

        首先指出,圖的任意一條最短路徑既不能包含負權(quán)回路,也不會包含正權(quán)回路,因此它最多包含|v|-1條邊。

其次,從源點s可達的所有頂點如果 存在最短路徑,則這些最短路徑構(gòu)成一個以s為根的最短路徑樹。Bellman-Ford算法的迭代松弛操作,實際上就是按頂點距離s的層次,逐層生成這棵最短路徑樹的過程。

在對每條邊進行1遍松弛的時候,生成了從s出發(fā),層次至多為1的那些樹枝。也就是說,找到了與s至多有1條邊相聯(lián)的那些頂點的最短路徑;對每條邊進行第2遍松弛的時候,生成了第2層次的樹枝,就是說找到了經(jīng)過2條邊相連的那些頂點的最短路徑……。因為最短路徑最多只包含|v|-1條邊,所以,只需要循環(huán)|v|-1 次。

每實施一次松弛操作,最短路徑樹上就會有一層頂點達到其最短距離,此后這層頂點的最短距離值就會一直保持不變,不再受后續(xù)松弛操作的影響。(但是,每次還要判斷松弛,這里浪費了大量的時間,這就是Bellman-Ford算法效率底下的原因,也正是SPFA優(yōu)化的所在)。

image,如圖(沒找到畫圖工具的射線),若是B和C的最短路徑不更新,那么點D的最短路徑肯定也無法更新,這就是優(yōu)化所在。

如果沒有負權(quán)回路,由于最短路徑樹的高度最多只能是|v|-1,所以最多經(jīng)過|v|-1遍松弛操作后,所有從s可達的頂點必將求出最短距離。如果 d[v]仍保持 +∞,則表明從s到v不可達。

如果有負權(quán)回路,那么第 |v|-1 遍松弛操作仍然會成功,這時,負權(quán)回路上的頂點不會收斂。

            參考來源:http://blog.sina.com.cn/s/blog_6803426101014l1v.html

        問題:Bellman-Ford算法是否一定要循環(huán)n-1次么?未必!其實只要在某次循環(huán)過程中,考慮每條邊后,都沒能改變當(dāng)前源點到所有頂點的最短路徑長度,那么Bellman-Ford算法就可以提前結(jié)束了(開篇提出的小優(yōu)化就是這個)。

        上代碼(來自牛帥)

#include
#include
using namespace std;
#define MAX 0x3f3f3f3f
#define N 1010
int nodenum, edgenum, original; //點,邊,起點
typedef struct Edge //邊
{
int u, v;
int cost;
}Edge;
Edge edge[N];
int dis[N], pre[N];
bool Bellman_Ford()
{
for(int i = 1; i <= nodenum;="" ++i)="">//初始化
dis[i] = (i == original ? 0 : MAX);
for(int i = 1; i <= nodenum="" -="" 1;="">
for(int j = 1; j <= edgenum;="">
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(順序一定不能反~)
{
dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
pre[edge[j].v] = edge[j].u;
}
bool flag = 1; //判斷是否含有負權(quán)回路
for(int i = 1; i <= edgenum;="">
if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
{
flag = 0;
break;
}
return flag;
}
void print_path(int root) //打印最短路的路徑(反向)
{
while(root != pre[root]) //前驅(qū)
{
printf('%d-->', root);
root = pre[root];
}
if(root == pre[root])
printf('%d\n', root);
}
int main()
{
scanf('%d%d%d', &nodenum, &edgenum, &original);
pre[original] = original;
for(int i = 1; i <= edgenum;="">
{
scanf('%d%d%d', &edge[i].u, &edge[i].v, &edge[i].cost);
}
if(Bellman_Ford())
for(int i = 1; i <= nodenum;="" ++i)="">//每個點最短路
{
printf('%d\n', dis[i]);
printf('Path:');
print_path(i);
}
else
printf('have negative circle\n');
return 0;
}

四.SPFA算法

        用一個隊列來進行維護。初始時將源加入隊列。每次從隊列中取出一個元素,并對所有與他相鄰的點進行松弛,若某個相鄰的點松弛成功,則將其入隊。直到隊列為空時算法結(jié)束;這個算法,簡單的說就是隊列優(yōu)化的bellman-ford,利用了每個點不會更新次數(shù)太多的特點發(fā)明的此算法(看我上面那個圖,只有相鄰點更新了,該點才有可能更新) 。

         代碼參見  http://www.cnblogs.com/hxsyl/p/3248391.html

五.趣聞

        整理該篇博文的時候,一哥們發(fā)布網(wǎng)站到我們?nèi)?,網(wǎng)站很精美,一牛神(acmol)使用fork炸彈,結(jié)果服務(wù)器立馬掛啦,更改后又掛啦,目測目前無限掛中。。。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多