ps:給17級講最短路徑時候自己寫的課件
目錄
最短路徑... 1
概述: 1
Floyd算法(弗洛伊德算法)復雜度O(n^3) 3
Dijkstra算法(迪傑斯特拉算法)復雜度O(nlog2n) 5
SPFA算法(Shortest Path Fast Algorithm的縮寫) 12
附錄:... 12
Floyd代碼... 12
Dijkstra O(n^2),鏈式前向星... 13
Dijkstra + priority_queue + 鏈式前向星... 15
最短路徑
概述:
假設有一個圖,點表示城市,邊表示路徑長度。
如圖,從點(2)到點(4)有若干總走法,比如
(2)->(3)->(4), 這么走路徑長度是4。
也可以(2)->(3)->(1)->(4) 這樣路徑長度是15。當圖有上萬個點的時候就無法用人工直接求任意兩點的路徑,哪一條是最短的。最短路徑就是解決這類問題。
例題:http://www.fjutacm.com/Problem.jsp?pid=1499
Floyd算法(弗洛伊德算法)復雜度O(n^3)
const int inf = 0x3f3f3f3f;
由於某些情況下避免正無窮加正無窮加成負無窮,所以正無窮有時不用0x7FFFFFFF, 用0x3f3f3f3f,或者1<<29, 1e9.
這個算法只能用鄰接矩陣存圖, 我們先初始化一下矩陣
void init() { ///初始化矩陣 for(int i = 1; i <= n; i ++ ) { for(int j = 1; j <= n; j ++ ) { if(i == j) { mp[i][j] = 0; ///自己到自己的距離0 } else { mp[i][j] = inf;///否則都是inf } } } } |
然后讀入一條u->v權值為w的邊。對於矩陣mp[u][v]記錄的是u->v的邊的長度。對於無向圖,mp[u][v]和mp[v][u]是相等的。而因為我們求的是最短路, u->v的路有多條我們只要存最短的。
scanf("%d %d %d", &u, &v, &w); mp[u][v] = mp[v][u] = min(mp[u][v], w); |
存好矩陣后我們跑floyd算法,核心代碼就這幾行
for(int k = 0; k < n; k ++ ) { for(int i = 0; i < n; i ++ ) { for(int j = 0; j < n; j ++ ) { mp[i][j] = min(mp[i][j], mp[i][k] + mp[k][j]); } } } |
然后矩陣mp[s][t]的值就是s->t的最短路徑。對於這題來說就是
printf("%d\n", mp[s][t] == inf ? -1 : mp[s][t]); |
下面我們來解釋一下floyd的三層循環。
我們發現(i)->(j)的最短路徑可能是之間i->j,也可能是經過中間點i->k->j。
如果我們用dp[i][j]表示u->v的最短路徑。那么
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
然后看循環。
當k = 0時
枚舉I,j.我們嘗試讓編號0的中點。優化我們的最短距離。
矩陣成為了經過0點優化過的最短距離。
K = 1時,我們又枚舉I,j。讓矩陣成為經過中間點0,1的最短路矩陣。
以此類推
。。。。
來自知乎的解釋
優點:好寫,核心代碼總共就4行。經常擴展出各種圖上動態規划
缺點:復雜度高。點的數量1000的圖就容易TLE
Dijkstra算法(迪傑斯特拉算法)復雜度O(nlog2n)
我們先有張圖,我們要求1->5的最短路徑是什么。
換個問題,我們要求一個dist數組,dist[i]表示起點到i的最短路徑。
如果起點是1,初始化dist[1] = 0,其余都是正無窮
我們的dist[]數組已經完成了dist[1](藍色),和1相連的有兩條邊。
我們更新dist數組后結果如上。
Dist[2] = min(dist[2], dist[1]+(1->2的權值))
Dist[3] = min(dist[3], dist[1]+(1->3的權值))
我們從數組中找一個最小的數值。上圖中的點2,那么dist[2]已經可以確定了。因為邊最短,走其他的路繞回來肯定路更長。
通過點2的邊,我們可以更新dist數組,如果邊能使得dist數值變小
(2)能到達3和4.
Dist[4]=min(dist[4], dist[2] + (2->4的權值))
Dist[3]=min(dist[3], dist[2] + (2->3的權值))
我們發現原來的dist[3]=4被dist[2]+(2->3的邊)=3覆蓋
Dist[4]=inf被dist[2]+(2->4的邊)=7覆蓋。
然后我們在選一條最短邊。
把點3加入集合。擴展的兩條邊更新dist數組。
繼續找最小
Dist數組計算完成。
在數據集合中選最小的數我們用優先隊列logn,每次都能確定一個點,總過n個點。總復雜度O(nlogn)
寫法和大部分和周三的prim算法幾乎一模一樣的,也分O(n^2)的版本和O(nlogn)優化的版本。一般肯定寫快的O(nlogn)的;
//比較挫的O(N^2)版本,可能比較好理解 int dist[maxn], vis[maxn]; int dijkstra(int s, int t) { //從s->t的最短路徑,無法到達反回-1 for(int i = 0; i < n; i ++ ) { dist[i] = inf, vis[i] = 0; } dist[s] = 0; for(int k = 1; k <= n; k ++ ) { ///dist數組n個,每次確定一個值 vis[s] = 1; for(int i = first[s]; ~i; i = edge[i].next) { ///更新dist數組 int to = edge[i].to, w = edge[i].w; if(!vis[to] && dist[to] > dist[s] + w) { dist[to] = dist[s] + w; } } int mincost = inf; for(int i = 0; i < n; i ++ ) { ///找最小 if(!vis[i] && dist[i] < mincost) { mincost = dist[i]; s = i; } } } if(dist[t] == inf) { return -1; } else { return dist[t]; } } |
//優先隊列實現 struct Node { int to, cost; Node() {} Node(int tt, int cc):to(tt), cost(cc) {} friend bool operator < (const Node &a, const Node &b) { return a.cost > b.cost; } };
int dist[maxn], vis[maxn];
int dijkstra(int s, int t) { for(int i = 0; i < n; i ++ ) { dist[i] = inf, vis[i] = 0; } priority_queue<Node>que; que.push(Node(s, 0)); /** 丟進去一個to=s,cost=0的結構體 等價於下面三行,看不懂往下翻附錄構造函數 struct Node tmp; tmp.to = s, tmp.cost = 0; que.push(tmp); */ while(!que.empty()) { Node now = que.top(); que.pop(); if(!vis[now.to]) { vis[now.to] = 1; dist[now.to] = now.cost; for(int i = first[now.to]; ~i; i = edge[i].next) { int to = edge[i].to, w = edge[i].w; if(!vis[to]) { que.push(Node(to, now.cost + w)); } } } } if(dist[t] == inf) { return -1; } else { return dist[t]; } } |
SPFA算法(Shortest Path Fast Algorithm的縮寫)
Ps: SPFA也叫bellman ford的隊列優化。但是bellman ford的復雜度比較高。SPFA的平均復雜度是O(n*log2n),復雜度不穩定,在稠密圖(邊多的圖)跑的比dijkstra慢,稀疏圖(邊少的圖)跑的比Dijkstra快。在完全圖達到最壞的平方級復雜度。我們學它是因為有些圖的邊的長度是負數。這時候dijkstra就GG了。
完全圖: n個點中如果每個點都與其他n-1有邊,無向圖中,就有n*(n-1)/2條邊。
我們要求一個dist[]數組表示起點,到其他點的最短路徑。
我們有一條u->v的邊,s是我們的起點。那么如果. S->V可以通過s->u,u->v來縮短距離那么就更新。我們稱之為【松弛】
我們可以每次對圖中每條邊u->v都拿出起點,松弛。直到每一次操作都不會改變dist數組的時候。Dist數組就是答案。(可以證明這樣最多我們只要進行n-1次操作,每次操作拿出全部的m條邊松弛,復雜度O(n*m),這就是bellman ford)。
但是,起點附近的松弛必定會影響其他點,起點沒松弛完,終點附近的變的松弛可以說是無效的。簡單來說,近的點的dist還沒確定,就用這個未確定的dist取更新遠的點,浪費時間。
所以我們需要一個隊列優化。
在隊列中的點,需要松弛。我們先把起點丟進隊列。具體看代碼。
Dist數組記錄距離,inq數組標記是否在隊列
int SPFA(int s, int t) { int dist[maxn], inq[maxn]; for(int i = 0; i < n; i ++ ) { dist[i] = inf, inq[i] = 0; } queue<int>que; que.push(s), inq[s] = 1, dist[s] = 0; while(!que.empty()) { int now = que.front(); que.pop(); inq[now] = 0; for(int i = first[now]; ~i; i = edge[i].next) { //每次拿出一個點開始松弛。 int to = edge[i].to, w = edge[i].w; if(dist[to] > dist[now] + w) { //這個if看下面的圖 dist[to] = dist[now] + w; if(!inq[to]) { //松弛過的點dist變換了,可能影響其他的點。需要繼續松弛 inq[to] = 1; que.push(to); } } } } return dist[t] == inf ? -1 : dist[t]; } |
附錄:
構造函數
平常我們寫結構體這么寫
struct Point {
int x, y;
};
Point a; //c++才能這么寫,c語言中要struct Point a;或者typedef后才能直接point a;
這是結構體a的值不確定。
struct Point {
int x, y;
Point() {}
};
其實它默認有一個沒有返回值,和結構體同名的函數,什么都不干。如果我們給他加點東西
struct Point {
int x, y;
Point() { x = 0; y = 0};
};
這樣Point a;那么a.x和a.y都是0.
如果我們這么寫
struct Point {
int x, y;
Point() { x = 0; y = 0};
Point(int xx, int yy) {
X = xx;
Y = yy;
}
};
那么Point a(2, 3), 點a的x就是2,y就是3。
所以 queue<Point >que; que.push(Point(2, 3));能直接往隊列丟一個(2,3)的結構體
Floyd代碼
#include <cstdio> #include <cstring> #include <algorithm> #include <iostream>
using namespace std; const int maxn = 1005; const int inf = 0x3f3f3f3f;
int n, m, mp[maxn][maxn];
void init() { for(int i = 0; i < n; i ++ ) { for(int j = 0; j < n; j ++ ) { if(i == j) { mp[i][j] = 0; } else { mp[i][j] = inf; } } } }
int main() { int u, v, w; while(~scanf("%d %d", &n, &m)) { init(); for(int i = 1; i <= m; i ++ ) { scanf("%d %d %d", &u, &v, &w); mp[u][v] = mp[v][u] = min(mp[u][v], w); }
for(int k = 0; k < n; k ++ ) { for(int i = 0; i < n; i ++ ) { for(int j = 0; j < n; j ++ ) { mp[i][j] = min(mp[i][j], mp[i][k] + mp[k][j]); } } }
int s, t; scanf("%d %d", &s, &t); printf("%d\n", mp[s][t] == inf ? -1 : mp[s][t]);
}
return 0; } |
Dijkstra O(n^2),鏈式前向星
#include <cstdio> #include <cstring> #include <algorithm> #include <iostream> #include <queue>
using namespace std; const int maxn = 1005; const int inf = 0x3f3f3f3f;
int n, m, first[maxn], sign;
struct Edge { int to, w, next; } edge[maxn * 2];
void init() { for(int i = 0; i < n; i ++ ) { first[i] = -1; } sign = 0; }
void add_edge(int u, int v, int w) { edge[sign].to = v; edge[sign].w = w; edge[sign].next = first[u]; first[u] = sign ++; }
int dist[maxn], vis[maxn];
int dijkstra(int s, int t) { for(int i = 0; i < n; i ++ ) { dist[i] = inf, vis[i] = 0; } dist[s] = 0; for(int k = 1; k <= n; k ++ ) { ///dist數組n個,每次確定一個值 vis[s] = 1; for(int i = first[s]; ~i; i = edge[i].next) { ///更新dist數組 int to = edge[i].to, w = edge[i].w; if(!vis[to] && dist[to] > dist[s] + w) { dist[to] = dist[s] + w; } } int mincost = inf; for(int i = 0; i < n; i ++ ) { ///找最小 if(!vis[i] && dist[i] < mincost) { mincost = dist[i]; s = i; } } } if(dist[t] == inf) { return -1; } else { return dist[t]; } }
int main() { int u, v, w; while(~scanf("%d %d", &n, &m)) { init(); for(int i = 1; i <= m; i ++ ) { scanf("%d %d %d", &u, &v, &w); add_edge(u, v, w); add_edge(v, u, w); } int s, t; scanf("%d %d", &s, &t); printf("%d\n", dijkstra(s, t)); }
return 0; } |
Dijkstra + priority_queue + 鏈式前向星
#include <cstdio> #include <cstring> #include <algorithm> #include <iostream> #include <queue>
using namespace std; const int maxn = 1005; const int inf = 0x3f3f3f3f;
int n, m, first[maxn], sign;
struct Edge { int to, w, next; } edge[maxn * 2];
void init() { for(int i = 0; i < n; i ++ ) { first[i] = -1; } sign = 0; }
void add_edge(int u, int v, int w) { edge[sign].to = v; edge[sign].w = w; edge[sign].next = first[u]; first[u] = sign ++; }
struct Node { int to, cost; Node() {} Node(int tt, int cc):to(tt), cost(cc) {} friend bool operator < (const Node &a, const Node &b) { return a.cost > b.cost; } };
int dist[maxn], vis[maxn];
int dijkstra(int s, int t) { for(int i = 0; i < n; i ++ ) { dist[i] = inf, vis[i] = 0; } priority_queue<Node>que; que.push(Node(s, 0)); while(!que.empty()) { Node now = que.top(); que.pop(); if(!vis[now.to]) { vis[now.to] = 1; dist[now.to] = now.cost; for(int i = first[now.to]; ~i; i = edge[i].next) { int to = edge[i].to, w = edge[i].w; if(!vis[to]) { que.push(Node(to, now.cost + w)); } } } } if(dist[t] == inf) { return -1; } else { return dist[t]; } }
int main() { int u, v, w; while(~scanf("%d %d", &n, &m)) { init(); for(int i = 1; i <= m; i ++ ) { scanf("%d %d %d", &u, &v, &w); add_edge(u, v, w); add_edge(v, u, w); } int s, t; scanf("%d %d", &s, &t); printf("%d\n", dijkstra(s, t)); }
return 0; } |
SPFA鏈式前向星
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <queue>
using namespace std; const int maxn = 205; const int maxm = 1005; const int inf = 0x3f3f3f3f;
int n, m, first[maxn], sign;
struct Edge { int to, w, next; } edge[maxn * maxn];
void init() { for(int i = 0; i < n; i ++ ) { first[i] = -1; } sign = 0; }
void add_edge(int u, int v, int w) { edge[sign].to = v; edge[sign].w = w; edge[sign].next = first[u]; first[u] = sign ++; }
int SPFA(int s, int t) { int dist[maxn], inq[maxn]; for(int i = 0; i < n; i ++ ) { dist[i] = inf, inq[i] = 0; } queue<int>que; que.push(s), inq[s] = 1, dist[s] = 0; while(!que.empty()) { int now = que.front(); que.pop(); inq[now] = 0; for(int i = first[now]; ~i; i = edge[i].next) { int to = edge[i].to, w = edge[i].w; if(dist[to] > dist[now] + w) { dist[to] = dist[now] + w; if(!inq[to]) { inq[to] = 1; que.push(to); } } } } return dist[t] == inf ? -1 : dist[t]; }
int main() { while(~scanf("%d %d", &n, &m)) { init(); for(int i = 1; i <= m; i ++ ) { int u, v, w; scanf("%d %d %d", &u, &v, &w); add_edge(u, v, w); add_edge(v, u, w); } int s, t; scanf("%d %d", &s, &t); printf("%d\n", SPFA(s, t)); }
return 0; } |