1. 程式人生 > >hdu 2544(所有最短路演算法總結)

hdu 2544(所有最短路演算法總結)

最短路

Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 82480    Accepted Submission(s): 35690

Problem Description

在每年的校賽裡,所有進入決賽的同學都會獲得一件很漂亮的t-shirt。但是每當我們的工作人員把上百件的衣服從商店運回到賽場的時候,卻是非常累的!所以現在他們想要尋找最短的從商店到賽場的路線,你可以幫助他們嗎?
 

Input

輸入包括多組資料。每組資料第一行是兩個整數N、M(N<=100,M<=10000),N表示成都的大街上有幾個路口,標號為1的路口是商店所在地,標號為N的路口是賽場所在地,M則表示在成都有幾條路。N=M=0表示輸入結束。接下來M行,每行包括3個整數A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口A與路口B之間有一條路,我們的工作人員需要C分鐘的時間走過這條路。
輸入保證至少存在1條商店到賽場的路線。

Output

對於每組輸入,輸出一行,表示工作人員從商店走到賽場的最短時間

Sample Input

2 11 2 33 31 2 52 3 53 1 20 0

Sample Output

32

 跟這道題打了三天架,終於把所有最短路演算法總結到一起,話不多說,來一波程式碼。

    Floyd:

/*Floyd是十分暴力的一個演算法,複雜度相當高,但是十分簡單,並且可以解決負權問題,
其主體思想為讓每個點都做一次跳板,去鬆弛所有的,這樣可以求出來任意兩個點之間
的最短路,用鄰接矩陣實現*/ 
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f     //概念上的無窮大 
int n,m,map[105][105];     //鄰接矩陣存地圖 
int main()
{
    int i,j,k;
    int x,y,z;
    while(cin>>n>>m && n || m)
    {
        for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
        {
            if(i==j)
            map[i][j]=0;
            else
            map[i][j]=inf;        //初始化 
        }
        
        for(i=1;i<=m;i++)
        {
            cin>>x>>y>>z;
            if(map[x][y]>z)      //解決重邊的問題 
            {
                map[x][y]=map[y][x]=z;     //構造無向圖 
            }
        }
        for(k=1;k<=n;k++)         //每個點都做一次跳板 
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=n;j++)
            {
                if(map[i][j]>map[i][k]+map[k][j])      //對圖中任意所有的點進行鬆弛 
                map[i][j]=map[i][k]+map[k][j];
            }
        }
        cout<<map[1][n]<<endl;       //直接輸出map[strat][end]就是最短路 
    }
    return 0;
}

不難看出這個的效率是十分低的,那讓我們來看另一種演算法

Dijkstra:

/*Dijstra相對於Floyd效率還是高了很多的,它的主題思想為
從源點出發,找距離它最短的一個點,再以這個點為跳板,找離跳板最近的點
當不能繼續進行時,再次回到源點,找第二小的點,重複操作,直到所有的點
都被訪問過為止,注意,每個點只能訪問一次,不然會和Floyd一樣複雜*/ 
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f
int map[105][105],dis[105],vis[105];     //map存地圖,dis存源點到當前點的距離,vis存訪問狀態 
int n,m;
void dijkstra(int start) 
{
    int i,j,k;
    for(i=1; i<=n; i++)
    {
        dis[i]=map[start][i];      //對dis進行初始化 
    }
    for(i=1;i<=n;i++)
    vis[i]=0;            //0表示沒有被訪問,1表示被訪問 
    vis[start]=1;
    for(i=1;i<=n-1;i++)   //因為最多訪問n-1個點,所以迴圈n-1次 
    {
        int mix=inf;
        int u;
        for(j=1;j<=n;j++)
        {
            if(vis[j]==0 && mix>dis[j])
            {
                mix=dis[j];          //記錄下距離源點最近的點,並且沒有被訪問 
                u=j; 
            }
        }
        vis[u]=1;      //標記為已經被訪問 
        for(k=1;k<=n;k++)
        {
            if(vis[k]==0 && dis[k]>dis[u]+map[u][k])
            dis[k]=dis[u]+map[u][k];            //以該點為跳板,對所有點進行鬆弛 
        }
    }
}
int main()
{
    int i,j,k;
    while(cin>>n>>m && n || m)
    {
        for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
        {
            if(i==j)
            map[i][j]=0;
            else
            map[i][j]=inf;     //初始化 
        }
        while(m--)
        {
            int x,y,z;
            cin>>x>>y>>z;
            if(map[x][y]>z)
            {
                map[x][y]=map[y][x]=z;     //無向圖 
            }
        }
        dijkstra(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

Dijkstra演算法雖然好,但是並不能解決負權問題,更準確的說是判斷有沒有負權的存在,於是就有了另一種演算法,Bellman_Fod:

/*Bellman_Ford的主體思想為對每一條邊進行鬆弛操作,並且是重複進行,最多  
重複n-1次,如果n-1後還可以進行鬆弛,說明有負權的存在,但是它並不能解決  
負權問題*/  
#include <iostream>  
using namespace std;  
#define inf 0x3f3f3f3f  
#define max 10000  
int u[2*max+10],v[2*max+10],w[2*max+10],dis[105];  
int main() {  //u存起點,v存終點,w存權值,dis和迪傑一樣,由於是無向圖,所以要 *2(我在這裡犯了一次錯)  
    int n,m,i,j,k;  
    int x,y,z;  
    while(cin>>n>>m && n || m) {  
        for(i=1; i<=2*m; i++) {  
            cin>>x>>y>>z;  
            u[i]=x;  
            v[i]=y;  
            w[i]=z;  
            j=i+1;       //構造無向圖  
            u[j]=y;  
            v[j]=x;  
            w[j]=z;  
            i++;  
        }  
        for(i=1; i<=n; i++)  
            dis[i]=inf;  
        dis[1]=0;     //將起點設定為零,這樣可以保證從起點開始鬆弛  
        for(k=1; k<=n-1; k++) { //最多迴圈n-1次  
            for(i=1; i<=2*m; i++) {  
                if(dis[v[i]]>dis[u[i]]+w[i])  
                    dis[v[i]]=dis[u[i]]+w[i];     //對所有的邊進行鬆弛操作  
            }  
        }  
        for(i=1; i<=2*m; i++) {  
            if(dis[v[i]]>dis[u[i]]+w[i])  
                return false;             //這裡檢測有沒有負權,不過本題用不到   
        }  
        cout<<dis[n]<<endl;  
    }  
    return 0;  
}  

看到這裡我想大家一定也發現了一個問題,寫法太樸素了,而且多了好多不必要的計算,那麼,我們就有對它們的優化,首先,先來看一下鄰接矩陣版本的Bellman_Ford的佇列優化,即SPFA 1.0:

/*SPFA相比較於之前的寫法減少了許多不必要的計算,其主體思路為,首先先將起點
放入佇列,然後將它拿出,對其他點進行鬆弛,如果鬆弛成功,則將其放進佇列(前提是它不在佇列中)
每一次都把隊首的點拿出來進行鬆弛,直到佇列為空,注意,點可以重複入隊,如果入隊超過n次,說明有
負權*/ 
#include <iostream>
#include <queue>
#include <string.h>
using namespace std;
#define inf 0x3f3f3f3f
int m,n;
int map[105][105],dis[105],vis[105],num[105];  //vis表示是否在佇列中,num表示入隊的次數 
queue<int> q;
int SPFA(int start)
{
    int i,j,k,temp;
    for(i=1;i<=n;i++)
    dis[i]=inf;
    memset(num,0,sizeof(num));
    memset(vis,0,sizeof(vis));
    dis[start]=0;        //將起點初始化為零,保證從這一點開始鬆弛 
    q.push(start);
    while(!q.empty())
    {
        temp=q.front();     //取隊首 
        q.pop();           //拿出 
        for(i=1;i<=n;i++)
        {
            if(dis[i]>map[temp][i]+dis[temp])
            {
                dis[i]=dis[temp]+map[temp][i];   //鬆弛 
                if(!vis[i])         //入隊 
                {
                    q.push(i);
                    vis[i]=1;
                    num[i]++;     //判斷是否存在負環 
                    if(num[i]>n)
                    return false;
                }
            }
            vis[temp]=0;     //標記出隊 
        }
    }
}
int main()
{
    int i,j,k;
    int a,b,c;
    while(cin>>n>>m && n || m)
    {
        for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
        {
            if(i==j)
            map[i][j]=0;
            else
            map[i][j]=inf;     //初始化 
        }
        for(i=1;i<=m;i++)
        {
            cin>>a>>b>>c;
            if(map[a][b]>c)
            map[a][b]=map[b][a]=c;    //無向圖 
        }
        SPFA(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

雖然說這解決了許多不必要的計算,但是用鄰接矩陣是十分浪費空間的,於是就有了SPFA2.0(鏈式前向星):

/*這種寫法的優勢在於可以節省大量的空間,用到了結構體,會用到一個head
陣列,初始化都為-1,代表沒有點與該點相連,如果有點與它相連,那就讓head
指向它,然後讓這個點的next(結構體變數)指向起點即-1,代表該點是最後一個
和頭點相接的點,如果有新的點進來,就把它插入到兩者之間,讓後進來的點指向
先進來的點,頭點(起點)指向新進來的點,這樣,最後一個點的next值一定為-1
這樣可以對每個邊進行鬆弛*/ 
#include <iostream>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define max 10000
int dis[105],head[105],vis[105];
int n,m,ans;
struct node {
    int v,w,next;     //v為要到達的點,w為權值,next為指向的下一個點 
} edge[2*max+5];      //無向圖 *2
void add(int from,int to,int cost) {
    edge[ans].v=to;
    edge[ans].w=cost;    //新增新的點 
    edge[ans].next=head[from];       //ans代表第幾條邊 
    head[from]=ans++;               //head指向最新的點(第幾條邊) 
}
bool spfa(int start) {
    for(int i=1; i<=n; i++)
        dis[i]=inf;  
    dis[start]=0;        //除源點外,其他均為inf 
    for(int i=1; i<=n; i++)
        vis[i]=0;
    queue<int> q;
    q.push(start);
    vis[start]=1;      //源點入隊 
    while(!q.empty()) {
        int temp=q.front();   //出隊 
        q.pop();
        vis[temp]=0;
        for(int i=head[temp]; i!=-1; i=edge[i].next) {   //對每個點所指向的點一一進行處理,i=-1時說明已經沒有點與之相連 
            int a=edge[i].v;
            int b=edge[i].w;
            if(dis[a]>dis[temp]+b) {
                dis[a]=dis[temp]+b;
                if(vis[a]==0) {
                    q.push(a);
                    vis[a]=1;
                }
            }
        }
    }
}
int main() {
    int x,y,z;
    int i,j,k;
    while(scanf("%d%d",&n,&m) !=EOF && n || m) {
        ans=1;
        for(i=1; i<=n; i++)
            head[i]=-1;
        for(i=1; i<=m; i++) {
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
            add(y,x,z);   //無向圖 
        }
        spfa(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

這個一樣可以判斷是否存在負環,我忘記謝了,操作和1.0一樣。

既然Bellman_Ford演算法可以進行優化,那麼Dijkstra一樣進行優化,用到了優先佇列:

/*Dijkstra優先佇列的優化,運用了過載函式可以優先使最大值或者最小值
出佇列的原理進行優化,節省了大量的空間和時間,主體思路為用vector分別
儲存邊和權,用結構體中的元素做跳板,操作過程為,先將起點放入優先佇列(先放到結構體中), 
然從該點開始找它可以鬆弛的點,然後全部暫時存到結構體中,然後放入優先佇列中,
然後將起點拿出,優先佇列自動接著會吧最小的拿出來進行鬆弛,直到佇列為空,
注意,一個點只能入隊一次*/ 
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
#define inf 0x3f3f3f3f
#define max 10000
int n,m,dis[105],vis[105];
vector<int> e[2*max+10];//邊     
vector<int> w[2*max+10];//權         //無向圖 *2 
struct P {
    int u,d;
    bool operator <(const P &a) const {
        return d>a.d;      //函式過載實現每次優先拿最小值 
    }
};
void dijkstra(int s) {
    int i,k,j;
    for(i=1; i<=n; i++)
        dis[i]=inf;   //初始化 
    dis[s]=0;
    memset(vis,0,sizeof(vis));
    priority_queue<P> q;
    P tn;
    tn.u=s;
    tn.d=0;   //暫時存到結構體裡面,以便優先佇列發揮作用 
    q.push(tn);
    while(!q.empty()) {
        P t;
        t=q.top();
        q.pop();
        if(vis[t.u]) continue;    //如果已經訪問過,直接取下一個點 
        vis[t.u]=1;
        int u=t.u;        
        for(i=0; i<e[u].size(); i++) {      //和它相連數量為size 
            int v=e[u][i];
            if(dis[v]>dis[u]+w[u][i]) {
                dis[v]=dis[u]+w[u][i];
                tn.d=dis[v];      //暫時存到結構體中,以便放入佇列 
                tn.u=v;
                q.push(tn);
            }
        }
    }
}
int main() {
    int i,j,k;
    int a,b,c;
    while(cin>>n>>m && n || m) {
        for(i=0; i<=2*m; i++) {
            e[i].clear();
            w[i].clear();      //一定要初始化,第一次我就因為這個WA了 
        }
        for(i=1; i<=m; i++) {
            cin>>a>>b>>c;
            e[a].push_back(b);
            w[a].push_back(c);
            e[b].push_back(a);    //無向圖 
            w[b].push_back(c);
        }
        dijkstra(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

同樣, 它也有鏈式前向星的寫法:

#include <iostream>
#include <algorithm>
#include <string.h>
#include <queue>
using namespace std;
#define inf 0x3f3f3f
long long dis[10005];
int head[10005];
int cnt;
struct node
{
	int v;
	int w;
	int next;
}edge[20005];
struct tnode
{
	int v;//當前點 
	int w;//當前點到起點的距離 
	friend bool operator < (tnode a,tnode b)
	{
		return a.w>b.w;
	}
}temp,now;//用於儲存暫時用到的點 
void add(int from,int to,int val)//鏈式前向星儲存 
{
	edge[cnt].v=to;
	edge[cnt].w=val;
	edge[cnt].next=head[from];
	head[from]=cnt++;
}
void dji(int s,int e)
{
	priority_queue<tnode> q;
	temp.v=s;//存起點 
	temp.w=0;
	q.push(temp);
	while(!q.empty())
	{	
		temp=q.top();//每次取距離起點最近的點 
		q.pop();
		int t=temp.v;
		if(dis[t]<temp.w) continue;//如果經過一個過渡點到達該點的距離小於起點直接到達該點的距離,那麼已經沒有必要再鬆弛 
		for(int i=head[t];i!=-1;i=edge[i].next)
		{
			now.v=edge[i].v;
			now.w=edge[i].w;
			if(dis[now.v]>dis[temp.v]+now.w)
			{
				dis[now.v]=dis[temp.v]+now.w;
				q.push(now);
			}
		}
	}
}
int main()
{
	int t,n,i,j,k;
	int a,b,c;
	while(cin>>n>>t)
	{
		cnt=1;
		memset(head,-1,sizeof(head));
		memset(dis,0x3f,sizeof(dis));
		dis[n]=0;
		while(t--)
		{
			cin>>a>>b>>c;
			add(a,b,c);
			add(b,a,c);
		}
		dji(1,n);
		cout<<dis[n]<<endl;
	}
	return 0;
}