圖論(三) (一) 最短路徑算法 1.Floyed-Warshall算法
這幾周開始正式系統學習圖論,新學期開始新的記錄。由於二模和生物地理兩門高考的臨近,時間比較倉促,所以暫時跳過圖論的(一)和(二),即圖的儲存和遍歷。從最短路徑算法學起,首先要學習的是Floyed-Warshall算法。
Floyed(佛洛依德)算法,是最簡單也是最基礎的最短路徑算法,可以計算圖中任意兩點間的最短路徑。佛洛依德算法的時間復雜度是O(N3),並且適用於出現邊的權為負數的情況,但是當圖中出現權為負數的回路時不建議使用此算法。
(其實學習算法的時候曾參考過大名鼎鼎的《算法導論》中關於此算法的介紹和講解,可惜並沒有堅持看懂,有機會再做詳解。)
以下為算法的描述和說明:
dis[u][v]表示從u到v最短路徑長度,m[u][v]表示連接u,v邊的長度。
描述:
1、初始化:點u,v有邊相連接,則dis[u][v]=w[u][v]. 如果之間沒有路徑,則dis[u][v]=INF.
2、for (k=1~n)
for(i=1~n)
for(j=1~n)
if(dis[i][j]>dis[i][k]+dis[k][j])
dis[i][j]=dis[i][k]+dis[k][j];
3、dis[i][j]的結果就是 i 到 j 的最短路徑。
說明:
第一層循環中間點k,第二、三層循環起始點和終點i,j. 思路大致是,如果點 i 到 點 k 的距離加上點 k 到點 j 的距離小於原來 i 到 j 的距離,那麽就用這個更短的路徑替換原來 i 到 j 的路徑。從算法描述中不難看出時間復雜的是O(N3
在提出例題前我想先問一個問題,在學習的時候也一直在思考這個問題。感覺時間復雜度如此高的一個算法為什麽仍一直在被人們使用呢?我想答案也正是這個算法的特色所在,即思路易理解,算法實現簡單,功能比較強大,而這不也是我們學習這門學科帶給我們的樂趣麽。
下面是一道例題:
農民 John的農場裏有很多牧區。有的路徑連接一些特定的牧區。一片所有連通的牧區稱為一個牧場。但是就目前而言,你能看到至少有兩個牧區通過任何路徑都不連通。這樣,Farmer John就有多個牧場了。
John想在牧場裏添加一條路徑(註意,恰好一條)。對這條路徑有以下限制:
一個牧場的直徑就是牧場中最遠的兩個牧區的距離(本題中所提到的所有距離指的都是最短的距離)。考慮如下的有5個牧區的牧場,牧區用“*”表示,路徑用直線表示。每一個牧區都有自己的坐標:
(15,15) (20,15)
D E
*-------*
| _/|
| _/ |
| _/ |
|/ |
*--------*-------*
A B C
(10,10) (15,10) (20,10)
【請將以上圖符復制到記事本中以求更好的觀看效果,下同】
這個牧場的直徑大約是12.07106, 最遠的兩個牧區是A和E,它們之間的最短路徑是A-B-E。
這裏是另一個牧場:
*F(30,15)
/
_/
_/
/
*------*
G H
(25,10) (30,10)
在目前的情景中,他剛好有兩個牧場。John將會在兩個牧場中各選一個牧區,然後用一條路徑連起來,使得連通後這個新的更大的牧場有最小的直徑。
註意,如果兩條路徑中途相交,我們不認為它們是連通的。只有兩條路徑在同一個牧區相交,我們才認為它們是連通的。
輸入文件包括牧區、它們各自的坐標,還有一個如下的對稱鄰接矩陣
:
A B C D E F G H
A 0 1 0 0 0 0 0 0
B 1 0 1 1 1 0 0 0
C 0 1 0 0 1 0 0 0
D 0 1 0 0 1 0 0 0
E 0 1 1 1 0 0 0 0
F 0 0 0 0 0 0 1 0
G 0 0 0 0 0 1 0 1
H 0 0 0 0 0 0 1 0
其他鄰接表中可能直接使用行列而不使用字母來表示每一個牧區。輸入數據中不包括牧區的名字。
輸入文件至少包括兩個不連通的牧區。
請編程找出一條連接兩個不同牧場的路徑,使得連上這條路徑後,這個更大的新牧場有最小的直徑。輸出在所有牧場中最小的可能的直徑。
輸入輸出格式
輸入格式:
第1行: 一個整數N (1 <= N <= 150), 表示牧區數
第2到N+1行: 每行兩個整數X,Y (0 <= X ,Y<= 100000), 表示N個牧區的坐標。註意每個 牧區的坐標都是不一樣的。
第N+2行到第2*N+1行: 每行包括N個數字(0或1) 表示如上文描述的對稱鄰接矩陣。
輸出格式:
只有一行,包括一個實數,表示所求直徑。數字保留六位小數。
只需要打到小數點後六位即可,不要做任何特別的四舍五入處理。
輸入輸出樣例
輸入樣例#1:8 10 10 15 10 20 10 15 15 20 15 30 15 25 10 30 10 01000000 10111000 01001000 01001000 01110000 00000010 00000101 00000010輸出樣例#1:
22.071068
由於本人水平有限,做這道題花了不少時間,主要遇到七七八八的問題在這裏也當作對我來說有價值的點羅列一下:
1、連接矩陣的讀入;
2、每個牧場的直徑;
3、連接路徑如何操作;
4、最後討論細節問題;
首先,看到連接矩陣中間沒有空格讀入,因此在這裏的處理我的想法是按照字符串一個一個讀取,判斷,處理數據.(題目見多了就不會出這個問題了)
第二個問題我一開始就腦子卡殼了,其實這就是用佛洛依德算法的最好時機啊。這個算法的厲害之處就在於它可以把所有點之間的最短路徑"安全"地儲存在一個數組裏,隨時可以調用,那麽牧場的直徑也就
較好求解了。用佛洛依德算法算出兩兩點之間的最短距離之後,將m[i]記作牧區 i 到可到達牧區中的每一個最短路徑中的最遠距離。而牧場的直徑自然為 d1=max{m[i]}。
第三個問題就是列舉沒有路徑相連接的牧區 i , j 。得到一條新牧場直徑的候選路徑,長度為 m[i]+m[j]+dist[i][j]。接下來的操作和上一步不同,這裏是從最長的距離中選出最短路徑,這樣才符合題意中直
徑的定義,則此時牧場的直徑為 d2=min{m[i]+m[j]+dist[i][j]}。
第四個問題,也是極容易忽略的一點,如果思維夠縝密,那麽不會漏掉這一討論,即有可能原來某一個牧場的直徑很長,要大於新牧場的直徑,那麽結果仍然為d1,因此最終的結果為d=max{d1,d2}。
下面附上代碼:
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #include<cmath> #include<cstdlib> using namespace std; const int maxn=150+10; const int inf=0x3f3f3f3f; struct node { int x; int y; }a[maxn]; double cal(int i,int j) { return sqrt((a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y)); } int n; double dis[maxn][maxn],ldis[maxn],l1,l2=inf,ans; int main() { int tmp; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d%d",&a[i].x,&a[i].y); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { scanf("%1d",&tmp); if(tmp)dis[i][j]=cal(i,j); else if(i!=j)dis[i][j]=inf; } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(dis[i][k]+dis[k][j]<dis[i][j]) dis[i][j]=dis[i][k]+dis[k][j]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { if(dis[i][j]!=inf)ldis[i]=max(dis[i][j],ldis[i]); l1=max(l1,ldis[i]); } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(dis[i][j]==inf) l2=min(ldis[i]+cal(i,j)+ldis[j],l2); ans=max(l1,l2); printf("%.6f",ans); return 0; }
應該是做題太少,這道題自我認為還是比較有味道的,可以反復看一下。
圖論(三) (一) 最短路徑算法 1.Floyed-Warshall算法