讓我們進入正題
最短路是啥
emmm 顧名思義最短路就是求一個點到另外一個點的最小距離
一般來說最短路分為:單源最短路和多源最短路
單源最短路就是求一個源點到另外多個點的最短距離
而多源最短路就是求多個點到其他點的最短距離
算法一般有:
- floyd(多源 O(\(n^3\)))
- dijkstra(單源 O(\(n^2\)) 可用堆優化到O(\(n*log_n\)))
- Bellman-Ford(單源 O(\(nm\)))
- SPFA(單源 O(\(nlog_m\) 最劣情況下會被卡到\(nm\)))
具體的優劣以及使用范圍我們會在下面具體講解
floyd 算法
- 適用范圍 : 多源最短路 可處理負權 但是不能處理負環 運行一次可求得任意兩點間最短路
- floyd算法其實很好理解 也很好寫(畢竟 O(\(n^3\)))
有的時候可以將其當作dp理解 - 先來想這樣一個問題 :
現在你在老家(B地)自由自在的玩耍着 突然有人告訴你去A地能給你分對象 (咳咳) 然后你就屁顛屁顛的跑去了A地 但是有好多人都要去A地 你希望可以最快到達A地(也就是路徑最短) - 顯然你可以直接坐車從B地直接趕往A地 但是這樣一定是最短的嗎?
然並卵 畢竟路上你要走山路十八彎 而這時C地出現在你的面前 從B直接到A要走1000km(反正很遠很遠) 但是從B到C只需要 1 km,從C到B呢也只需要 1 km(反正很短很短)那你肯定會先到C 再到B吧 - 這就是我們的核心思路了
揪黑板!!
如果我們已知並記錄了從i到j的最短路徑 而如果將k作為中轉點可以使得我們的最短路徑更短 那我們就更新i到j的最短路徑 (其他算法也會用到這個思想,即下面的松弛操作)
核心代碼實現:
for(int k=1;k<=n;++k)//枚舉中轉點
for(int i=1;i<=n;++i)//枚舉邊的起點
for(int j=1;j<=n;++j)//枚舉邊的終點
if(a[i][j]>a[i][k]+a[k][j])//松弛操作(即利用第三個點來判斷是否可以更新目標兩個點的最短距離)
a[i][j]=a[i][k]+a[k][j];//a[i][j]是從i到j的最小值
關於k為什么要枚舉在第一層循環:
剛才已經說過floyd類似於dp,而k就是dp的階段(dp的階段顯然要枚舉在第一層的),其實a本來是三維a[k][i][j]表示只經過前k個點從i到j的最短路,而可以將第一維的k舍去(like背包) 所以就成了現在的樣子啦
dijkstra 算法
- 適用范圍:單源最短路 不能處理帶有負權邊的圖 需要指定起點s
- dij是求最短樓最常用的方法也是最經典的:
然后維護一個集合S用於存放已經知道對於源點s的最短距離的點
另外一個集合U用於維護還不知道對於源點s的最短距離的點(但是可以知道當前不完全狀態下的最短距離) - 初始時 S中只有s自己 距離自己的距離是0 而其他點距離s的距離都初始化為正無窮
然后我們利用這個點來求出對於其他點的最短距離
- 首先進行一次松弛操作 將s可以直接到達的點的距離dis[i]記錄下來 然后更新i點的距離(如果比當前已知的s到i的最短距離更短的話)
- 從U集合中選出一個距離s最短的點 將其加入到S集合中 然后利用這個點再去更新另外一些點的距離
- 在新出現的點中選出距離s最短的點 加入到S集合中 然后再利用新點再去更新其他點距離
- 重復以上步驟知道目標點距離源點s的距離求出或者無法再更新
- 下面是圖例演示
核心代碼實現 :
void dij(int s){
memset(vis,0,sizeof(vis));
vis[s] = 1;//將s放入S集合
for(int i = 1;i <= n;++i){
if(g[s][i]){dis[i] = g[s][i];}//如果從s到i有路的話 就將s到i的距離設置為長度
else dis[i] = 0x3f3f3f3f;//將其他點設置為正無窮(即目前無法到達)
}
dis[s] = 0;//源點s到自己的最短距離是0
for(int i = 1;i < n;++i){//遍歷每一個點以求出每一個點距離源點的最小距離
int Min = 0x3f3f3f3f,k = 0;//Min維護這一輪維護后要放入S集合的距離最小值,k維護要放入S集合的點
for(int j = 1;j <= n;++j)
if(!vis[j] && Min > dis[j]){//如果點j還沒有在S集合中並且s到當前節點的距離更小
Min = dis[j];k = j;
}
vis[k] = 1;//k放入S集合
for(int j = 1;j <= n;++j){
if(g[k][j] && dis[j] > dis[k] + g[k][j]){//如果可以通過k松弛
dis[j] = dis[k] + g[k][j];//更新到j的最小值
}
}
}
}
關於優化:
- 上一個只是朴素的最短路算法 有的時候並不能滿足我們的要求(
和出題人喪心病狂的卡空間時間) - 顯然我們的時間瓶頸在於找出最小值 每次找出一個最小值需要n的時間來實現 而每次只會更新一個點的距離 所以最終的時間復雜度會是O(\(nlog_n\))
- 如果我們開一個小根堆來實現 每次找到最小值只需要log的時間 那么就可以優化成(nlog_n)了
核心代碼實現:
void dij(int x){
priority_queue<node> q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
dis[x] = 0;
q.push(node(x,0));
while(!q.empty()){
node t = q.top();q.pop();
int k = t.num;
if(vis[k])continue;
vis[k] = 1;
for(int i = head[k];i;i = a[i].next){
int v = a[i].to;
if(dis[v] > dis[k] + a[i].dis){
dis[v] = dis[k] + a[i].dis;q.push(node(v,dis[k] + a[i].dis));
}
}
}
}
Bellman-ford算法
- 適用范圍 : 基本啥也能用 (前提是不考慮時間復雜度情況下)
- 算法思想:和dij很像 但是這里是沿着邊進行松弛操作
- 對於有向帶權圖, 從源點s開始,利用Bellman-ford,依次求解各頂點的最短距離,
算法概況:
for(int i = 0;i < n;++i)//枚舉頂點
for each(i,j)//對於每一條邊
song_chi(i,j)//松弛操作
- BF算法對每一條邊做松弛操作 , 並且重復了n次,因此算法的時間復雜度為O(n*E)
核心代碼實現:
void BF(int u){
memset(d,0x3f,sizeof(d));
d[u] = 0;
for(int i = 1;i < n;++i){
for(int j = 1;j <= cnt;++j){//cnt存的是圖中共有幾個邊
int x = a[j].from,y = a[j].to,z = a[j].dis;
d[y] = min(d[y],d[x] + z);//松弛操作
}
}
}
- ~~買一送一 BF算法更加實惠 ~~
咳咳 既然dij和BF感覺實現方法差不多 但是BF有一個dij不能企及的地方:判負環
如果我們通過BF算法求得了各個點到源點s的最短路 然后再進行一次松弛呢?
如果有負環的話是不是我們會再重新跑一遍負環然后讓各個點的值更小? 所以利用這個性質我們就可以來判斷是否有負環啦!
bool check(){
for(int i = 1;i <= cnt;++i){
int x = a[i].from,y = a[i].to,z = a[i].dis;
if(d[y] < d[x] + z)return 1;
}
return 0;
}
//主函數中:
if(check()){
printf("NO\n");return 0;
}
SPFA算法
- 適用范圍:反正BF能用的它都能用(文章開頭說過它可以看成是BF的優化)
- BF每次都通過所有的邊來松弛出一個新點的最短距離 但是這樣太浪費了
- 只有那些已經松弛過的點才可能去松弛別的點,所以我們可以用一個隊列來記錄松弛成功了的點,以此用這些點來松弛鄰接點 一樣會優化到O(nlog) 但是對於特殊構造的數據會被卡到O(nm) 所以可以加一些優化 比如雙端隊列優化
核心代碼實現
struct node{
int to,dis,next;
}a[maxn];
void add(int x,int y,int z){
a[++cnt].to = y;a[cnt].next = head[x];a[cnt].dis = z;head[x] = cnt;
}
bool spfa(int s){
memset(dis,0x3f,sizeof(dis));dis[s] = 0;//dis存到源點的最短距離
queue<int>q;
q.push(s);flag[s] = 1;//s入隊
while(!q.empty()){
int u = q.front();q.pop();flag[u] = 0;//因為一個節點u可能多次進隊
for(int i = head[u];i;i = a[i].next){//鄰接表存邊
int v = a[i].to;
if(dis[v] > dis[u] + a[i].dis){//松弛操作:沒錯,還是我!!!
dis[v] = dis[u] + a[i].dis;
if(!flag[v]){//優化
if(++num[v] >= n)return 0;如果同一個點被多次松弛 那么肯定有負環(這個判斷也比剛才的少女口阿 把前輩666扣在公屏上)
q.push(v);flag[v] = 1;//v進隊,標記
}
}
}
}
return 1;
}
//主函數中:
if(!spfa(源點))輸出NO
else 輸出距離
// 雙端隊列優化版
// 題目是洛谷P3371
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5+10;
int n,m,cnt;
int head[maxn],dis[maxn],vis[maxn];
struct node{
int next,to,dis;
}a[maxn];
void add(int x,int y,int z){
a[++cnt].to = y;
a[cnt].next = head[x];
a[cnt].dis = z;
head[x] = cnt;
}
deque<int> q;
void spfa(int s){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s] = 0;vis[s] = 1;
q.push_front(s);
while(!q.empty()){
int u = q.front();q.pop_front();
vis[u] = 0;
for(int i = head[u];i;i = a[i].next){
int v = a[i].to;
if(dis[v] > dis[u] + a[i].dis){
dis[v] = dis[u] + a[i].dis;
if(!vis[v]){
vis[v] = 1;
if(!q.empty() && dis[v] < dis[q.front()])q.push_front(v);
else q.push_back(v);
}
}
}
}
}
int main(){
int s;scanf("%d%d%d",&n,&m,&s);
for(int i = 1;i <= m;++i){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
spfa(s);
for(int i = 1;i <= n;++i){
if(dis[i] == 1061109567)printf("2147483647 ");
else printf("%d ",dis[i]);
}
return 0;
}
如果您有不懂的地方 或者 您發現代碼有問題可以在下方評論或者給博主留言
感謝觀看>_<