一. Djikstra算法定義
形式:
用來解決單源最短路徑的問題,即給出圖G和起點s,通過算法到達每個頂點的最短距離。
基本思想:
對圖G(V, E)設置集合S, 存放已被訪問的頂點,然后每次從集合V-S中選擇與起點s的最短距離最小的一個頂點u,訪問並加入集合S。之后,令頂點u為中介點, 優化起點和所有的從u能到達的頂點v之間的最短距離。這樣的操作執行n(頂點的個數)次。
偽代碼:
//G為圖, 一般設置為全局變量,數組d為源點到達各點的最短路徑長度,s為起點
Djikstra(G, d[], s){
初始化
for(循環n次){
u = 是d[u]最小的還未被訪問的頂點的標號
記u已被訪問
for(從u出發能到達的所有頂點v){
if(v未被訪問&&以U為中介使得s到頂點v的最短距離d[v]更優){
優化d[v]
}
}
}
}
二、具體實現
1. 鄰接矩陣版
const int MAXV = 1000;//最大頂點數
const int INF = 10000000000;//設INF為一個很大數
//適用於點數不大的情況
int n, G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV];
void Dijkstra(int s){
fill(d, d+MAXV, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1. MIN = INF;//u使d[u]最小, MIN存放該最小d[u]
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小於INF的d[u],說明剩下的頂點與s不連通
if(u == -1) return;
vis[u] = true;//標記u為已訪問
for(int v = 0; v < n; v++){
//如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//優化d[v]
}
}
}
}
2. 鄰接表版
struct node{
int v, dis;//v為邊的目標頂點,dis為邊權
};
vector<node> Adj[MAXV];
int n;
int d[MAXn];
bool vis[MAXV] = {false};
void Dijkstra(int s){
fill(d, d+MAXV, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
//只有這個部分與鄰接矩陣自而發不同
for(int j = 0; j < Adj[u].size(); j++){
int v = Adj[u][j].v//通過鄰接表直接獲得u能夠到達的v
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
d[v] = d[u] + Adj[u][j].dis;
}
}
}
}
3. 最短路徑的求法(在上面的基礎上)
- 如果題目中給出的是無向圖,那么根據鄰接表和鄰接矩陣自行改變即可,如果是鄰接矩陣可以在兩邊同時加上一樣的權值;如果是鄰接表那么使用push_back,也是同時交換輸入。
<1>以鄰接矩陣為例
const int MAXV = 1000;//最大頂點數
const int INF = 10000000000;//設INF為一個很大數
//適用於點數不大的情況
int n, G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV];
int pre[MAXV];//表示從起點到頂點v的最短路徑上v的前一個頂點(新添加)
void Dijkstra(int s){
fill(d, d+MAXV, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1. MIN = INF;//u使d[u]最小, MIN存放該最小d[u]
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小於INF的d[u],說明剩下的頂點與s不連通
if(u == -1) return;
vis[u] = true;//標記u為已訪問
for(int v = 0; v < n; v++){
//如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//優化d[v]
//就是在這里改變,添加了一條語句
pre[v] = u;//新添加
}
}
}
}
//如何打印,就是遞歸打印
void dfs(int s, int v){
if(v == s){
printf("%d\n", s);
return;
}
dfs(s, pre[v]);
printf("%d\n", v);
}
4. 會出現的其他情況,即附加條件
- 即出現除了第一個條件最短路徑外,可能還會有其他的條件限制,如有多條路徑
- 每條邊增加一個邊權(花費);
- 每個點增加一個點權(如每個城市可以收到的物資);
- 直接問有多少條最短路徑。
<1>新增邊權。以新增邊權花費為例,都是在最后判斷出進行修改,其余均不需改動,因為是新增邊權,所以在存儲上和原來的最短路徑是一樣的,就是新開辟數組cost[][],然后設立數組c,用於存儲最小花費。初始化c[s]=0,其余都初始化為c[u]=INF.
for(int v = 0; v < n; v++){
//如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//優化d[v]
c[v] = c[u] + cost[u][v];
}else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]){
c[v] = c[u] + cost[u][v];
}
}
}
<2>新增點權,以新增的點權代表城市中能收集到的物資為例,用weight[u]表示城市u中物資數目,並增加一個數組w[],令其為起點s到到達頂點u可以收集的最大物資w[u].初始化w[s] = weight[s], 其余w[u] = 0.
for(int v = 0; v < n; v++){
//如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//優化d[v]
w[v] = w[u] + weight[v];
}else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]){
w[v] = w[u] + weight[v];
}
}
}
<3>求最短路徑的條數。只需要增加一個數組num[],初始時num[s] = 1,其余都為num[u] = 0.
for(int v = 0; v < n; v++){
//如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//優化d[v]
num[v] = num[u];
}else if(d[u] + G[u][v] == d[v]){
num[v] += num[u];
}
}
}
五、更優的遍歷模板(Djikstra+DFS)
- 只考慮最短路徑(距離)的Djikstra算法,然后從這些最短距離中選出一條第二尺度最優的路徑出來。
1. 使用Djikstra算法記錄所有最短的路徑
- 我們需要使用vector,來記錄最短路徑的前驅結點,因為最短路徑的條數可能不止一條。
- 通過vector類型的數組就可以通過DFS來獲取所有的最短路徑。
- 首先講解如何獲取這個pre數組,本部分的Djikstra算法只需考慮距離這一因素,不必受第二尺度的干擾,之前的寫法中,數組初始化pre[i] = i,表示在初始條件下的前驅為自身,但是在這不需要進行初始化,因為可能會進行更新操作。
代碼:
vector<int> pre[MAXN];
void Djikstra(int s){
fill(d, d+MAXN, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
for(int v = 0; v < n; v++){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
pre[v].clear();//更新清空操作,這就是為啥不用初始化。
pre[v].push_back(u);
}else if(d[u] + G[u][v] == d[v]){
pre[v].push_back(u);
}
}
}
}
}
2. 遍歷所有最短路徑找出一條使第二尺度最優的路徑
- 與之前不同的是,之前的寫法是使用一個遞歸來獲取最短路徑,,但是現在可能存在多個前驅結點,所以遍歷的過程就是會形成一棵遞歸樹。
- 當對於這棵樹進行遍歷的時候,每次到達葉子結點,也就是起點,就會產生一條完整的最短路徑。
- 得到最短路徑后,可以進行第二尺度的計算,然后進行跟新,找出最優的最短路徑。
- 接下來考慮如何進行DFS函數的書寫:
- 最為全局變量的最優值optValue
- 記錄最優路徑的數組path(使用vector)
- 臨時記錄DFS遍歷到葉子結點時的路徑tempPath(也使用vector記錄)
- 然后就是遞歸邊界和遞歸式
- 遞歸邊界,如果當前訪問的結點是葉子結點(也就是路徑的起點st),那么說明到達遞歸邊界,此時tempPath,存放了一天最短路徑,求出第二尺度value的值,與optValue進行比較,如果更優那么進行覆蓋。
- 對於遞歸式,只需對於pre[v]中的所有前驅結點進行遍歷即可。
- 遞歸過程中tempPath是如何生成的,只需在訪問結點v時,將v添加到tempPath中即可,然后遍歷遞歸,等pre[v]中所有結點遍歷完畢后,把tempPath最后面的v彈出。
- 需要注意的是,葉子結點(也就是路徑的起點st),沒有辦法通過上的寫法直接加入tempPath,因此在訪問到葉子節點的時候臨時加入。
代碼:
int optValue;
vector<int> pre[MAXN];
vector<int> path, tempPath;
void DFS(int v){//當前訪問結點v
//遞歸邊界
if(st == v){//如果到達葉子結點st(即路徑起點)
tempPath.push_back(v);
int value;//存放臨時路徑tempPath的第二尺度的值
計算路徑tempPath上的value
if(value 優於 optValue){//更新第二尺度最優值與最優路徑
optValue = value;
path = tempPath;
}
tempPath.pop_back();//將剛加入的結點刪除
return;
}
//遞歸式
tempPath.push_back(v);//將當前訪問結點加入臨時路徑的最后
for(int i = 0; i < pre[v].size(); i++){
DFS(pre[v][i]);
}
tempPath.pop_back();//遍歷完所有前驅結點,將當前結點v刪除
}
- 對於上面部分,我們只需要針對於題目的要求,對計算路徑tempPath的value進行修改即可,而這個地方一般會涉及路徑邊權或者點權的計算,需要注意的是,由於遞歸的原因,存放在tempPath中的路徑結點是逆序的,因此訪問結點需要倒着進行,當然如果僅是對於邊權或點權進行求和,正着訪問也是沒有問題的。
//邊權之和
int value = 0;
for(int i = tempPath.size()-1; i > 0; i--){
//當前結點id, 下一個結點idNext
int id = tempPath[i], idNext = tempPath[i-1];
value += V[id][idNext];//value增加邊id->idNext的邊權
}
//點權之和
int value = 0;
for(int i = tempPath.size()-1; i >= 0; i--){
int id = tempPath[i];
value += W[id];
}