ACM演算法練習題單:最短路問題 & 總結
在之前不久總算是結束了對最短路4四個演算法的練習,在剛開始的時候每次都是拿到一題就一定要用參考書本把4個演算法都敲一遍才對自己覺得放心,到最後上來就看資料規模(如果規模小就用Floyd-Warshall)然後就看是否有負圈,最後直接用SPFA的套路。首先我先列出我在練習期間所做的部分題單。(以POJ為主)
1. POJ 1860 Currency Exchange :
題意:已經給出了不同種類的貨幣之間的匯率,問:是否能夠通過不同貨幣之間的不斷轉換而盈利。
想法:對於第一次做最短路的人不建議做這題。雖然這是裸最短路求負圈,但是,對於第一次做最短路的我來說很難一眼看出這是最短路問題。
2. POJ 3259 Wormholes :
題意:在農場中有單向的蟲洞也有雙向的路徑,通過因為通過蟲洞會回到之前的時間。問:能否通過這些蟲洞使得時間不斷的倒流。
想法:這題是一個比較明顯的最短路求負圈的題,但是,使用Floyd演算法會超時。
3. POJ 1062 昂貴的聘禮 :
題意:中文題,題意省略。
想法:是一個比較明顯的最短路問題。但是,對於資料的儲存、使用和程式碼實現稍微有點麻煩。建議多做些最短路問題後在返回來完成它。
4. POJ 2253 Frogger:
題意:水面上有很多石頭,一隻青蛙在某一塊石頭上,現在這隻青蛙需要跳到另外一塊石頭上去看它的朋友,問:在這途中的最大距離最小是多少。
想法:這是最典型的最短路問題,只需在資料的存貯和程式碼實現上稍作修改即可。
5. POJ 1125 Stockbroker Grapevine :
題意:現在我們需要把一個訊息在一群人中快速傳播開,即所有人都知道這個訊息。問:最快讓所有人都知道訊息的時間,和最後一個知道訊息的人。
想法:這是一眼就能看出來的最短路問題。
6. POJ 2240 Arbitrage:
題意:給出所有貨幣名稱和它們之間的匯率和手續費,問:能否通過不同貨幣之間的不斷轉換盈利
想法:這題和前面的POJ 1860相似,只是這個題在儲存上稍微麻煩點。<我是用map儲存的>
7. POJ 1511 Invitation Cards:
題意:已知單向的公交車路線和坐車的價格,問:去赴宴然後回來的最低消費。
想法:這是一個比較好的最短路問題,因為它的資料規模比較大。如果沒有這題我不會去學習SPFA的。
8. POJ 2387 Til the Cows Come Home :
題意:已知一個雙向的路線,問:最短路的路程。
想法:這是典型的最短路問題。
9. POJ 3037 Skiing :
題意:已知地形的各個點的高度,初始速度。問:從左上角出發,到右下角時的最快速度。
想法:我當時做這題是怎麼看都不像是最短路問題,之後瞟了一眼別人的思路後,才恍然大悟。真是非常好的最短路問題。
10.POJ 3615 Cow Hurdles:
題意:已知兩個點之間的柵欄高度。問:在兩點間的最高的柵欄高度的最小值。(可以間接到達。)
想法:通過這個題馬上就體現出了Floyd演算法的優勢,這真是專為Floyd演算法準備的題啊。
11.POJ 3660 Cow Contest :
題意:已知N頭牛之間的M次比試的結果,其中等級高的必定會勝過等級低的。問:通過這些比試記錄能確定幾頭牛的等級。
想法:這是一個與上面第四題相似的最短路問題。同時還能快速瞭解什麼是傳遞閉包,怎麼通過Floyd解決。
12.POJ 3013 Big Christmas Tree:
題意:已知一棵樹的各個節點權值和邊的權值。問:從根到所有節點的分值的最小值。
想法:這也是一個數據比較大的最短路問題,雖然只是資料比較大。
13.POJ 3159 Candies:
題意:在n個人中有m條關係,每條關係是表示為 A B k (B最多隻能比A多k個糖)。問:1最多比n多幾顆糖。
想法:這個題真心非常好,這是一個典型的最短路問題中差分約束題。想解決這題需要稍微拐個彎。
下面是我個人在練習完最短路問題後的小總結,以及我給我自己的最短路模板。
不過剛開始自己琢磨四個演算法的時候真是越看越覺得神奇,現在,粗略理解了四個演算法後,我個人對四個演算法的使用做出瞭如下總結:
1.Bellman-Ford演算法:
這是我學習的第一個最短路演算法,它屬於單源最短路演算法。這個演算法所利用的是任意兩點之間最短路的更新有限的特點,(更新次數就是點的數量。)同時如果存在負圈就會無限更新。(根據這個特點可以用來判斷是否存在負圈。)在不斷的練習後差不多就會寫出一個屬於自己的程式碼風格吧!下面的是給我自己模板:(以無向圖為例。)
#include<iostream>
#define INF 0x3f3f3f3f
/*因為練習期間查了很多的網上程式碼他們用的都是一個很大的數字
在一次偶然的情況下看到了kuangbin的寫的模板,於是就看了下這
個值發現0x3f3f3f3f的大小是1061109567,這個大小剛剛好非常適
合使用如果小了在很多情況下就不夠用,如果大了就容易溢位。*/
#define MAX 1000 //特殊情況特殊分析
using namespace std;
struct edge{
int from,to,cost;
};
edge E[MAX*6];
int n,m,tol,start,en;
int cost[MAX];
void fill()
{//因為其他演算法中基本上都要對很多東西進行初始化,所以乾脆就
//直接用這種方式進行初始花了萬一寫到一半想換個演算法了呢?
for(int j=1;j<=n;j++)
cost[j]=INF;
}
void Bellman_Ford(int start)
{
cost[start]=0; //起點到起點的自然就是0了。
bool update=true; //用來判斷每次玄幻中是否更新。
int count=0; //用count記錄更新的次數如果更新次數過多就說明出現了負圈。
while(update){
if(count==n){ //更新次數過多說明已經出現了負圈。
cout<<"出現負圈"<<endl;return;
}
count++;
update=false;
for(int j=0;j<tol;j++){
if(cost[E[j].to]>cost[E[j].from]+E[j].cost){
cost[E[j].to]=cost[E[j].from]+E[j].cost;
update=true;
}
if(cost[E[j].from]>cost[E[j].to]+E[j].cost)
{//如果是定向圖的就把這個掉。
cost[E[j].from]=cost[E[j].to]+E[j].cost;
update=true;
}
}
}
return;
}
int main()
{
while(cin>>n>>m){
fill();
tol=0;
for(int j=0;j<m;j++){
int from,to,cost;
cin>>from>>to>>cost;
E[tol].from=from,E[tol].to=to,E[tol].cost=cost;
tol++;
}
cin>>start>>en;
Bellman_Ford(start);
cout<<cost[en]<<endl;
}
}
演算法複雜度為O(n*m)並不是任何情況下都適用,不過在可以使用的情況下用來判斷是否有負圈還是不錯的選擇。
2.Dijkstra演算法:
Dijkstra演算法採用的是鄰接表/鄰接矩陣的做法,也是單源最短路演算法。它的思路是在沒有負圈的情況下,每次只要找出已知答案中最小的一個來進行更新即可。不過正應如此採用Dijkstra演算法是無法判斷負圈的存在,但是,我們大天朝的一個大學生在n年前就在這個基礎上想出了一個時間複雜度更短而且能判斷負圈的SPFA演算法。同時使用不同的方式建立鄰接表/鄰接矩陣也會對程式碼的優劣性產生很大影響。下面是使用鄰接矩陣方法寫的一個我個人的模板:(還是以無向圖為例)
#include<iostream>
#include<cstring>
#define INF 0x3f3f3f3f
#define MAX 1000
using namespace std;
int n,m;
int V[MAX][MAX],cost[MAX];
bool used[MAX];
void fill()
{//這裡要對cost(總路程),used(是否被使用過),V(所有的邊)進行初始化。
memset(V,-1,sizeof(V));
for(int j=1;j<=n;j++){
cost[j]=INF;
used[j]=false;
}
}
int min(int a,int b){ return a>b?b:a; }
void Dijkstra(int start)
{
cost[start]=0; //起點設定路程為0。
while(true){
int a=-1;
for(int j=1;j<=n;j++)
//通過這個找出最小的一個,在SPFA中對這一步直接就使用了優先佇列。
if(!used[j]&&(a==-1||cost[a]>cost[j])) a=j;
if(a==-1) break;
//如果沒有找到,就說明能找的都已經標記過了所以跳出迴圈
used[a]=true; //找出來之後就進行標記。
for(int j=1;j<=n;j++){ //然後對到所有點的路程進行更新。
if(V[a][j]!=-1) //如果沒有a->j的路徑就可以忽略。
cost[j]=min(cost[j],cost[a]+V[a][j]);
}
}
return;
}
int main()
{
while(cin>>n>>m){
fill();
int from,to,val;
for(int j=0;j<m;j++){
cin>>from>>to>>val;
V[from][to]=val;
V[to][from]=val; //如果是定向圖就去掉這句。
}
int start,en;
cin>>start>>en;
Dijkstra(start);
cout<<cost[en]<<endl;
}
}
這個演算法有很大的優化空間,比如可以把鄰接矩陣換成使用鄰接表,不過這兩個就各有優劣了,所以,還是要具體情況具體分析。時間和空間發雜度都為O(n*n)。
3.Floyd-Warshall演算法:
Floyd-Warshall演算法簡稱Floyd演算法,它是一個多源最短路演算法,同時還能用來判斷負圈的存在。但是,它也具有很大的侷限性。它的思路就是使用了DP的做法。因為它的實現比較簡單,而且程式碼簡短。所以,在資料規模比較小時比較常用。下面是我個人的模板:(以無向圖為例)
#include<iostream>
#include<cstring>
#define MAX 100
#define INF 0x3f3f3f3f
using namespace std;
int cost[MAX][MAX];
int n,m;
int min(int a,int b) { return a>b?b:a; }
void Floyd()
{
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
if(cost[j][i]!=-1&&cost[i][k]!=-1){
if(cost[j][k]==-1)
cost[j][k]=cost[j][i]+cost[i][k];
else
cost[j][k]=min(cost[j][k],cost[j][i]+cost[i][k]);
if(cost[j][k]<-1&&j==k)
cout<<"存在負圈"<<endl;
}
}
}
}
return;
}
int main()
{
while(cin>>n>>m){
memset(cost,-1,sizeof(cost));
for(int j=1;j<=n;j++) cost[j][j]=0;
for(int j=0;j<m;j++){
cin>>from>>to>>val;
cost[from][to]=min(cost[from][to],val);
cost[to][from]=min(cost[to][from],val);//如果是定向圖解刪除這一句。
}
Floyd();
for(int j=1;j<=n;j++)
{//這個演算法結束後就可以得到所有點之間的最短路程。
for(int k=1;k<=n;k++){
cout<<cost[j][k]<<" ";
}cout<<endl;
}
}
}
三層for迴圈O(n*n*n )的複雜度,但是,思路簡單程式碼實現也比較簡單。所以,只有在資料規模比較小的情況下才會使用。
4.SPFA演算法
這個演算法應該算是我們現在使用的最多的了,同時也是我最喜歡用的一個。就是因為它的時間發雜度比較低。它的思路就是在Dijkstra的基礎上加上優先佇列,同時在很多時候如果能在加上鄰接表就可以是時間複雜度再降低一個等級。下面是我個人的模板:(由於使用的是鄰接表所以下面的就是以定向圖為例。)
#include<iostream>
#include<cstring>
#include<queue>
#define MAX 5005
#define INF 0x3f3f3f3f
using namespace std;
struct edge{
int from;
int to,cost;
};
struct Qpoit{ //在優先佇列中的值是點和對應的路程。
int k,w;
};
bool operator < (const Qpoit & p1,const Qpoit & p2){ //優先佇列從小取到大。
return p1.w>p2.w;
}
edge E[MAX*2];
int head[MAX],cost[MAX]; //用head和結構體來得到鄰接表。
bool Bque[MAX]; //使用這個來記錄對應的點是否在佇列中
int tol;
int n,m;
void add_edge(int from,int to,int cost){ //加入的過程中更新鄰接表
E[tol].to=to,E[tol].cost=cost,E[tol].from=head[from];
head[from]=tol++;
}
void fill() //初始化
{
for(int j=1;j<=n;j++){
head[j]=-1;
Bque[j]=false;
cost[j]=INF;
}
}
void SPFA(int start)
{
priority_queue<Qpoit> que;
Qpoit st,en;
st.k=start,st.w=0;
cost[start]=0;
que.push(st);
while(!que.empty()){
st=que.top();que.pop();//這樣st中取出的直接就是最小的。同時取出後必須馬上彈出。
for(int j=head[st.k];j!=-1;j=E[j].from){ //在這裡就已經在使用鄰接表遍歷了
en.k=E[j].to;
if(cost[en.k]>cost[st.k]+E[j].cost){
cost[en.k]=cost[st.k]+E[j].cost;
en.w=cost[en.k];
que.push(en); //如果使用佇列優化則,在加入佇列前需要判斷它是否在佇列中。
}
}
}
return;
}
int main()
{
while(cin>>n>>m){
fill();
tol=0;
for(int j=0;j<m;j++){
int from,to,cost;
cin>>from>>to>>cost;
add_edge(from,to,cost);
add_edge(to,from,cost);//如果是有向圖就可以把這句去掉。
}
int start;
cin>>start;
SPFA(start);
//在SPFA演算法結束後輸出每個點到起點的距離。
for(int j=1;j<=n;j++)
cout<<cost[j]<<" ";
cout<<endl;
}
}
這個SPFA中取數字的方式有很多除了使用優先佇列以外還有佇列、陣列、堆、棧等,總之它們的作用的就是不斷的更新每個點到起點的路程。同時更新後又將它加入到佇列/陣列/堆/棧中。(需要注意如果是用佇列優化則在加入佇列前還需要判斷它是否在佇列中。)
上面四個程式碼中的內容對於時間複雜度上並不是最優的,在練習期間因為使用cin超時的不在少數,(因為,在輸入過程中輸入的量太大。)所以,一般情況下,我還會備一個輸入輸出外掛。而且上面的四個程式碼,是我再不斷的練習後所形成的一種寫的方式,優化的空間本身就還有很大。(而且說不定有些時候還不一定對。)總是最短路演算法就是在每次都不斷的更新每個點到起點的之間的距離。
另外,在練習最短路的過程中下面這個網址中的內容對我也起到了很大的幫助。所以順便就貼上這個網址。
求最短路徑的四種演算法的詳細講解:HDU 1874 (最短路)Floyd-->>Dijkstra-->>Bellman_Ford-->>SPFA
http://blog.sina.com.cn/s/blog_7b7c7c5f01011yuu.html