迪傑斯特拉(Dijkstra) —— 最短路演算法
Dijkstra是最短路基礎演算法之一(還有判負環的SPFA和多源最短路的Floyd),但在正常情況下Dijkstra是最快的,也同樣是最難打的(其實都不是很難),接下來我們來談談具體演算法:
1.適用範圍:沒有負環(就是走一圈就可以將路程變小,沒有最短路的圖)的單源最短路(就是隻有一個起點的最短路);
2.思路:
已知量只有每條邊的權值,但我們可以很容易的想到起點到起點到起點的最短路是0,於是我們可以一開始就將定義一個dij陣列來存從起點到每個點的最短路的長度,所以自然dij[1](假設1是起點)就為0,而想要更新其他的就要將除1以外的賦值為MAXN;
因為沒有負的權值,所以很容易證明在已有的dij陣列中的最小值是不會再被其他點更新的,所以每一次我們都將已有的最短的那個數對應的節點取出,再由它來更新它所連線的點(如果到最小值的那個點的值加上權值依然小於它所連線的那個節點的已有的最小值就更新它),同樣不能忘了將這個最小值的點凍結(開個bool陣列)以免重複計算;
由於每次取出一個點又凍住那個點,我們不難想到在有n個點的情況下只需要迴圈n次就行了;
3.操作:
輸入:先用鄰接表)存下圖(注意是單向圖還是雙向),記錄下起點終點;
初值:先memset(dij,0x3f,sizeof(dij)),再將dij[1]賦值為0;
Dijkstra:用一個迴圈n次的for迴圈來做取點以及凍結的操作,每次取出最小的點再迴圈尋找每一個與它相連的節點並更新需要更新的節點;
輸出:按題目要求輸出;
特殊處理(輸出路徑):既然都已經找到最短路了那麼路徑只需要再另開一個數組在更新最短路長度的同時記錄是哪一個點更新的它就行了,在最後再用倒推的方式輸出即可(可以類比揹包問題找方案的方法);
4.例題:
騎車比賽
Description
小信準備去參加騎車比賽,比賽在 n 個城市間進行,編號從 1 到 n。選手們都從城市 1 出發,終點在城市 n。
已知城市間有 m 條道路,每條道路連線兩個城市,注意道路是雙向的。現在小信知道了他經過每條道路需要花費的時間,他想請你幫他計算一下,他這次比賽最少需要花多少時間完成。
Input
第一行輸入兩個整數 n,m(1≤n≤1,000,1≤m≤5,000),分別代表城市個數和道路總數。接下來輸入 m 行,每行輸入三個數字 a,b,c(1≤a,b≤n,1≤c≤200),分別代表道路的起點和道路的終點,以及小信騎車通過這條道路需要花費的時間。保證輸入的圖是連通的。
Output
輸出一行,輸出一個整數,輸出小信完成比賽需要的最少時間。
Sample Input 1
5 6 1 2 2 2 3 3 2 5 5 3 4 2 3 5 1 4 5 1
Sample Output 1
6
Dijkstra的經典例題(裸題),思路就不在贅述,直接上程式碼:
程式碼1(無優化):
#include<bits/stdc++.h>
using namespace std;
struct node//結構體代表每個節點的資料 ;
{
int to,ti;//to代表到達的節點,ti代表所需時間 ;
node(int a,int b)//建構函式(不會可以不用)
{
to=a;
ti=b;
}
node(){}
};
int main()
{
int n,m,x,y,z;
cin>>n>>m;
map<int,vector<node> > ma;//用對映來存節點以及的其對應的邊 ;
for(int i=1;i<=m;i++)
{
cin>>x>>y>>z;
ma[x].push_back(node(y,z));//注意本題是雙向圖 ;
ma[y].push_back(node(x,z));
}
int st=1,dis[n+1],vis[n+1];//st代表目前最小的值的節點一開始當然是1;
memset(dis,0x3f,sizeof(dis));//先賦一個極大值 ;
memset(vis,0,sizeof(vis));//表示是否凍結了節點 ;
dis[1]=0;//起點為0;
for(int j=1;j<=n;j++)//迴圈取(凍)點 ;
{//cout<<st<<endl;
int minn=1e+8,tmp;
vis[st]=1;//凍結節點 ;
int l=ma[st].size();
for(int i=0;i<l;i++)//列舉、更新 ;
{
if(!vis[ma[st][i].to])
{
dis[ma[st][i].to]=min(dis[ma[st][i].to],dis[st]+ma[st][i].ti);
}
}
for(int i=1;i<=n;i++)//找已有的最小值 ;
{
if(!vis[i]&&dis[i]<minn)
{
minn=dis[i];
tmp=i;
}
}
st=tmp;//更新 ;
}
cout<<dis[n];
}
我們再仔細讀程式碼便可以發現,找最小值的操作會浪費大量時間(O(n)),那可不可以優化成O(1)呢??我們可以用一個神奇的資料型別——set,set有一個天然排序的性質,我們稱這種優化為堆優化;
堆優化程式碼:
#include<bits/stdc++.h>
using namespace std;
struct node//結構體代表每條邊的資料 ;
{
int to,q,next;//to表示邊連線的節點,q代表權值,next代表這條邊的下一個邊 ;
node(int a,int b,int c)
{
to=a;
q=b;
next=c;
}
node(){}
}ma[1000001];//ma用於存邊 ;
set<pair<int,int> > st;//優化的set,分別存dij[i]和i ;
int k,head[1000001],dij[1000001],n,m;//head表示每一個節點,由head找到節點對應的邊 ;
bool ok[1000001];
void add(int a,int b,int c)//用於存a到b用c時間的邊
{
ma[++k]=node(b,c,head[a]);
head[a]=k;
}
int main()
{
memset(dij,0x3f,sizeof(dij));
memset(head,-1,sizeof(head));
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
dij[1]=0;
st.insert(make_pair(0,1));
for(int i=1;i<=n;i++)
{
int u=st.begin()->second;//->為指標找到最小值的節點 ;
ok[u]=1;//凍結節點
st.erase(st.begin());//刪除節點
for(int j=head[u];~j;j=ma[j].next)//~j表示j是否為-1 ;
{
if(!ok[ma[j].to]&&dij[u]+ma[j].q<dij[ma[j].to])
{
st.erase(make_pair(dij[ma[j].to],ma[j].to));
dij[ma[j].to]=ma[j].q+dij[u];
st.insert(make_pair(dij[ma[j].to],ma[j].to));
}
}
}
cout<<dij[n];
}