最短路徑算法

問題描述:

最短路徑問題是圖論研究中的一個經典算法問題, 旨在尋找圖(由結點和路徑組成的)中兩結點之間的最短路徑。

所謂單源最短路徑問題是指:已知圖G=(V,E),我們希望找出從某給定的源結點S∈V到V中的每個結點的最短路徑。
首先,我們可以發現有這樣一個事實:如果P是G中從vs到vj的最短路,vi是P中的一個點,那麼,從vs沿P到vi的路是從vs到vi的最短路。
最常用的路徑算法有:Dijkstra算法、A*算法、SPFA算法、Bellman-Ford算法、Floyd-Warshall算法、Johnson算法
以上是參考百度百科,以下分別論述閉包最短通路長度、Dijkstra算法和Floyd算法。

1.閉包最短通路長度

        定理:設G是帶有相對於頂點順序的鄰接矩陣A的圖。從的長度爲r的不同通路的數目等於的第(i,j)項,其中r爲正整數。
其證明參考離散數學第六版,9.4.6。要理解這個過程也不難,可以從矩陣乘法原理去理解,從公式上理解就是(i,j)=(i,k)*(k,j),如果是通路,它自然不爲0了,相當於計數了。從這個定理可以看出,我們在計算時,如果(i,j)的值由0變成非0,此時就是兩頂點之間通路長度,即需要至少分幾段才能連接兩點。最典型的應用是換乘次數最少的路徑。

2.Dijkstra算法

         Dijkstra(迪傑斯特拉)算法是典型的單源最短路徑算法,用於計算一個節點到其他所有節點的最短路徑。主要特點是以起始點爲中心向外層層擴展,直到擴展到終點爲止。Dijkstra算法是很有代表性的最短路徑算法,在很多專業課程中都作爲基本內容有詳細的介紹,如數據結構,圖論,運籌學等等。注意該算法要求圖中不存在負權邊。
        算法思想:設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分成兩組,第一組爲已求出最短路徑的頂點集合(用S表示,初始時S中只有一個源點,以後每求得一條最短路徑 , 就將加入到集合S中,直到全部頂點都加入到S中,算法就結束了),第二組爲其餘未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度,U中的頂點的距離,是從v到此頂點只包括S中的頂點爲中間頂點的當前最短路徑長度。
        算法步驟:
        a.初始時,S只包含源點,即S={v},v的距離爲0。U包含除v外的其他頂點,即:U={其餘頂點},若v與U中頂點u有邊,則w<u,v>正常有權值,若u不是v的出邊鄰接點,則w<u,v>權值爲∞。同時設每個頂點的最優值在L(u)中,L(vi)=無窮,L(起始點)=0;
       b.從U中選取一個距離v最小的頂點k,把k,加入S中(該選定的距離就是v到k的最短路徑長度)。
       c.以k爲新考慮的中間點,修改U中各頂點的距離;若從源點v到頂點u的距離(經過頂點k)比原來距離(不經過頂點k)短,則修改頂點u的距離值,修改後的距離值的頂點k的距離加上邊上的權。即L(v)=min(L(v),L(u)+w(u,v))
       d.重複步驟b和c直到所有頂點都包含在S中。


       理解起來也不是很難,就是從起始點開始,向周邊找到最短距離,並記錄到每個點,然後找到最短的路重新向周邊找且記錄。不過對於已經選擇的最優點就不要重新計算了。
       爲什麼這麼做,就能找出最優解呢,這裏可以先看我的另一篇博文《動態規劃-鋼條切割》。裏面提到,如果一個路徑是最優到,那路徑中的每一個點到終點也是最優的。同樣,我們反過來走也是一樣的。但這裏和那裏面又有點不一樣,那裏面是,一層一層地求最優解,即相隔1,2,3....點這樣求,但實際上這沒有廣泛的適合性,當網絡連接相當複雜時,你無法知道一點到另外一點的最優值,即使是相鄰,如果你真的已經求出來了,恭喜你,你明白Dijkstra算法的精髓了。
        正因爲我們時常不能很嚴格地將圖分爲一層一層的,所以要找到每一個點的最優解時,理解起來比較困難。那麼我們怎麼找到一個點的最優解呢,怎麼樣在不知道其它點的最優值時,求出這個點的最優解呢?其實我們人生也是如此,我們想每個選擇都最優,其實很難做到。其實,只要大致方向上是對的,踏踏實實裏往前走,時刻調整人生的方向,就能走上人生巔峯,迎娶白富美。這裏有三個關鍵,一是大致方向上是對的,不能南轅北轍,二是踏踏實實,三是時刻調整。對,做到這三點很難做到吧,這個算法就教你怎麼做到的。
        首先,在起始點找到局部最優解(當然每個相鄰點都要計算,你不嘗試你怎麼知道誰好誰壞呢?所以人生貴在敢於嘗試,而且永遠都不會忘記這種感覺,因爲後面還要用到的),然後定位在那個點,這不難吧。圖中標號爲2就是,然後以這個點又找到局部最優解,點3。我們就會驚訝發現,原來從點2出發後,轉了一圈發現點3又是最優的,說明點2的最優解的探索過程完了,7就是點2的最優解。既然找到了點2的最優解,自然以後就不管它嘍,這就是算法中的集合S和U的作用。是吧,在外面轉了一圖,發現還是點3好,跟談戀愛一樣,雖說初戀不一定很優秀,但是最想念的哈。那我們繼續從點3出發找最優解,發現點6是最優的,不過點6已經不是以前的點6了,因爲它改了。既然點3不是最優的,說明點3的命運到頭了,因爲它不能繼續保持領先地位,自然要讓給下一位,同時點3也完成了自己最優解的使命了。
         這樣,一步步地就把每個點的最優解求出來了,同時也不需要計算出每個點的所有情況再作比較,只需要證明它周邊有比它更優秀的,來說明它不是所要找的最優路徑中的點。同時也發現,它再怎麼繼續轉悠,也不可能找到比它現在還小的數。有人就要問,這不是還沒更新麼,等之前的點更新之後,再反過來找此點,說不定能找到更小的。這看起來挺有道理的,其實不然,在局部的最優解中,其它的點即使是更新了最優值,同樣要比此值要小,爲什麼呢,無論怎麼更新,它始終要從它到初始點的最路徑,加上某個值才使它進一步小,話說回來,它頂多是從別的點折返回來,就如點6還得從1-3,3-6給折回去,這樣一來,怎麼可能有1-2直接最優呢。所以呢,當轉悠了一圈後,發現周邊還有比我們強的,說明我們自己已經是最優的了,不需要去再去折騰了。這個時候,要麼把棒子交給下一個人,要自己繼續學習,使自己比以前的自己更強大才行,這就是我們平時說的,當才華不能支撐起野心時,是時候安靜下來好好學習了。
         這裏,還說一句,爲什麼這個算法不適合有負數的權值的呢?很簡單,因爲它依靠的是最優子序列所推測出來的,所以當存在負數時,這就不適用了。不然好不容易把了個最優的,發現下一個權值是負數,這說明上一個找的不是最優的,這不矛盾嗎,是不是哈。
        那路徑怎麼確定呢?L(v)=min(L(v),L(u)+w(u,v)),每一次求每個頂點的最優值,都會產生一個u,即最優點的前一個點,如果這個頂點的值如果後面一直不變,那麼它的前驅始終爲u,但有改變,則它也改變,所以只要跟蹤每一個點的前驅,按圖索驥,就可以找到最優路徑。
         說到這裏,大家應該明白爲什麼要搞集合S和U了吧,下面就是C程序代碼:
//path的每個值初始化爲-1,後面打印時用的
int dijkstra(int **w,int *path,int n,int begin,int end)
{
	begin-=1;end-=1;
	int i;
	bool *S=new bool[n];//S,L與文章中的意義一樣,這裏沒有U,因爲U是S的餘集
	int *L=new int[n];
	for(i=0;i<n;i++)//除了起始點爲0,其它點的最優值初始爲無窮大
	{
		L[i]=MAXLEN;
		path[i]=-1;//初始化path
	}
	L[begin]=0;
	memset(S,0,n*sizeof(S[0]));

	int u=begin,iu;
	int tmp;
	while(!S[end])
	{
		S[u]=true;
		for(i=0;i<n;i++)
			if(!S[i]&&L[u]+w[u][i]<L[i])//u與v的局部最優解,如果有,則替換,!S[i]代表U中的元素
			{
				L[i]=L[u]+w[u][i];
				path[i]=u;//記錄前驅
			}

		tmp=MAXLEN;
		iu=u;
		for(i=0;i<n;i++)
			if(!S[i]&&L[i]!=MAXLEN&&L[i]<tmp)//找出U中元素中最小最優值,即局部最優值
			{
				iu=i;//記錄是哪個點爲局部最優值,然後存入S中
				tmp=L[i];
			}
		u=iu;	
	}
	return L[end];
}

void printPath(int *path,int n,int begin,int end)
{
	begin-=1;end-=1;
	int u=end;
	while(-1!=path[u])//逆序打印,path的每個值初始化爲-1,-1代表起始點
	{
		cout<<u+1<<"<=";
		u=path[u];
	}
	cout<<begin+1<<endl;
}


3.Floyd算法

       正如大多數教材中所講到的,求單源點無負邊最短路徑用Dijkstra,而求所有點最短路徑用Floyd。確實,我們將用到Floyd算法,但是,並不是說所有情況下Floyd都是最佳選擇。     對於沒有學過Floyd的人來說,在掌握了Dijkstra之後遇到All-Pairs最短路徑問題的第一反應可能會是:計算所有點的單源點最短路徑,不就可以得到所有點的最短路徑了嗎。簡單得描述一下算法就是執行n次Dijkstra算法。 
        Floyd可以說是Warshall算法的擴展了,三個for循環便可以解決一個複雜的問題,應該說是十分經典的。從它的三層循環可以看出,它的複雜度是n3,除了在第二層for中加點判斷可以略微提高效率,幾乎沒有其他辦法再減少它的複雜度。 
        比較兩種算法,不難得出以下的結論:對於稀疏的圖,採用n次Dijkstra比較出色,對於茂密的圖,可以使用Floyd算法。另外,Floyd可以處理帶負邊的圖。  

    下面對Floyd算法進行介紹: 
        Floyd算法的基本思想:     可以將問題分解,先找出最短的距離,然後在考慮如何找出對應的行進路線。如何找出最短路徑呢,這裏還是用到動態規劃的知識,對於任何一個城市而言,i到j的最短距離不外乎存在經過i與j之間的k和不經過k兩種可能,所以可以令k=1,2,3,...,n(n是城市的數目),在檢查d(ij)與d(ik)+d(kj)的值;在此d(ik)與d(kj)分別是目前爲止所知道的i到k與k到j的最短距離,因此d(ik)+d(kj)就是i到j經過k的最短距離。所以,若有d(ij)>d(ik)+d(kj),就表示從i出發經過k再到j的距離要比原來的i到j距離短,自然把i到j的d(ij)重寫爲d(ik)+d(kj),每當一個k查完了,d(ij)就是目前的i到j的最短距離。重複這一過程,最後當查完所有的k時,d(ij)裏面存放的就是i到j之間的最短距離了。這樣我們就可以用3個for循環就可以完成了。
for ( int i = 0; i < 節點個數; ++i )
{
    for ( int j = 0; j < 節點個數; ++j )
    {
        for ( int k = 0; k < 節點個數; ++k )
        {
            if ( Dis[i][k] + Dis[k][j] < Dis[i][j] )
            {
                // 找到更短路徑
                Dis[i][j] = Dis[i][k] + Dis[k][j];
            }
        }
    }
}
    但是這裏我們要注意循環的嵌套順序,如果把檢查所有節點X放在最內層,那麼結果將是不正確的,爲什麼呢?因爲這樣便過早的把i到j的最短路徑確定下來了,而當後面存在更短的路徑時,已經不再會更新了。具體來講,這就是固定兩端i,j,還是固定中間k的選擇問題。這兩個有什麼區別呢。如果是固定兩端,當k遍歷完時,ij之間的最短距離就算完了,這對嗎?很顯然此時每個結點的最優值都還不確定呢,那結果就不一定對的,特別是算的越早的越不準。但如果我們把k固定,固定變化i,j,那麼每一次都會把所有點經過k的點的最短距離算出來,當k算完,所有點的結果就算完了。這就是所有點對一個點的最小距離算完後,每個點都刷新一次,而前一種,每個點的刷新次數不一樣,這公平嗎?哈哈!
    接下來就要看一看如何找出最短路徑所行經的城市了,這裏要用到另一個矩陣P,它的定義是這樣的:p(ij)的值如果爲p,就表示i到j的最短行經爲i->p...->j,也就是說p是i到j的最短行徑中的j之前的第一個城市。P矩陣的初值爲p(ij)=j。有了這個矩陣之後,要找最短路徑就輕而易舉了。對於i到j而言找出p(ij),令爲q,就知道了路徑i->q->...->j;再去找p(qj),再去找p(qj),如果值爲r,i到q的最短路徑爲q->r...->j;所以一再反覆,到了某個p(tj)的值爲j時,就表示t到j的最短路徑爲t->j,就會的到答案了,i到j的最短行徑爲i->q->r...->t->j。
     但是,如何動態的回填P矩陣的值呢?回想一下,當d(ij)>d(ik)+d(kj)時,就要讓i到j的最短路徑改爲走i->...->k->...->j這一條路,但是d(kj)的值是已知的,換句話說,就是k->...->j這條路是已知的,所以k->...->j這條路上j的上一個城市(即p(kj))也是已知的,當然,因爲要改走i->...->k->...->j這一條路,j的上一個城市正好是p(kj)。所以一旦發現d(ij)>d(ik)+d(kj),就把p(kj)存入p(ij)。
     可以這麼想當d(ij)>d(ik)+d(kj)時,也就是說i->...->k->...->j,那麼k應該在什麼位置呢?我們知道path(i,j)一定由原先的值變化而來的,而且是由上一個path變化而來的,如果每一次變化都會造成i->k->...->j或i->...->k->j,這樣的話,我們就可以遞推得到我們所想要路徑。那麼如何才能這樣,而不是i->...->k->...->j中間的任何一個呢?path(i,j)=path(?,?)呢?是path(i,k),還是path(k,j)?我們可以這麼想,如果發生了d(ij)>d(ik)+d(kj)這麼一個東西,拋開是不是最短的路徑,但i,j,k之定通路,不僅是通路,而且,每次變化的路徑是有一定的繼承性的,因爲它至少是經過第k點的最優路徑。當k+1時,如果值沒發生變化,則變化的是k~j而不是i~j,如果發生了變化了,說明上一個不是最佳的,需要調整。所以每次變化的一定是i->k->...->j或i->...->k->j。下面有一個例子說明:

      現在分析2~1之間的最短路徑。k=1,2時,2到1之間的路徑依然是無窮,因爲是它們本身的節點,但此時別其它點已經計算到1,2暫時的最佳路徑,比如3~1。當k=3時,2~3的路徑計算出來,同時3~1的之前就計算出來了,所以些Path(2,1)=3。當k=4的時候,3~1的最佳路徑發生變化,但與2~1的最佳無關,除非能夠計算出來2~4~1的路徑更短。如果你仔細思考的話,會發現如果path(i,j)只變化一次的話,那它的路徑就是i~k~j,至於k~j是多少,那個是後續的事,如果變化了多次,它依然是i~k~j,因爲些算法只計算2個長度的路徑,所以每次的k一定會挨着i。會不會發生i~k,和k~j同時發生呢,會!但如果i~k發生了變化,那麼k~j原先就會失效,得重新尋找更新了,所以從不變的角度來講,i~k這個過程始終不會發生變化,這樣我們可以通過尋找i~k~j,k~v~j,....找到我們所需要的路徑。所以只需要將path(i,j)=path(i,k)這樣賦值就可以了,如果是path(i,j)=path(k,j),這就和剛纔分析是反過來了,同時初值做一次翻轉。
      上面解決了,那麼path如何賦初值呢?那就看k=1是啥情況嘍,很明顯,Path(i,1)=1,因爲i~1~1嘛,所以有path(i,j)=j。這樣我們就得到其C語言程序了,如下:
#define MAXLEN 10000
void initPath(int **path,int n)//初始化path
{
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			path[i][j]=j;
}

void floyd(int **d,int **path,int n)
{
	int tmp;
	for(int k=0;k<n;k++)
		for(int i=0;i<n;i++)
			for(int j=0;j<n;j++)
			{
				tmp=d[i][k]+d[k][j];
				if(tmp<d[i][j])
				{
					d[i][j]=tmp;
					path[i][j]=path[i][k];//記錄i~k之間的路徑
				}

			}
}

void printPath(int **d,int **path,int n,int begin,int end)
{
	begin-=1;end-=1;
	if(MAXLEN==d[begin][end])//這裏如果不是通路,就會出現這情況
		cout<<"There is no path from "<<begin<<" to "<<end<<endl;
	else
		cout<<"The distance of the shortest path from "<<begin+1<<" to "<<end+1
		<<" is "<<d[begin][end]<<" !"<<endl;
	cout<<begin+1<<"->";//打印原點
	int v=path[begin][end];//獲得第一個路徑頂點下標
	while(v!=end)//如果路徑頂點下標不是終點
	{
		cout<<v+1<<"->";//打印路徑頂點
		v=path[v][end];//獲得下一個路徑頂點下標
	}
	cout<<end+1<<endl;//打印終點
}

以上皆用到

#define MAXLEN 10000

4.參考資料:

http://developer.51cto.com/art/201403/433874.htm
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html
http://www.cnblogs.com/twjcnblog/archive/2011/09/07/2170306.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章