最短路(搬運)
(1) Floyd
時間複雜度:\(O(n^3)\)
全源最短路
可處理負邊權
upd on 2021/10/7:
把原鄰接矩陣看作一個矩陣。
與廣義矩陣乘法的結合律完美地契合。
模板:
//dis[i][j]表示:經由編號k或k之前的點,從i到j的最短路徑
for(int k=1;k<=n;k++)//k列舉中轉點*
for(int i=1;i<=n;i++)//起點
for(int j=1;j<=n;j++)//終點
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
*中轉點:可以利用的點,並且可以按照其他順序(如時間軸)列舉(參見災後重建
(2) Bellman-Ford
時間複雜度:O(nm)
思路:沒有什麼最短路問題是鬆弛解決不了的,如果有,那就來n-1次
模板:
for(int i=1;i<=n-1;i++){//鬆弛次數
for(int j=1;j<=m;j++){//列舉每條邊
dis[v[j]]=min(dis[v[j]],dis[u[j]]+w[j]);
}
}
而且還能判負環
for(int j=1;j<=m;j++){
if(dis[f[j][2]]>dis[f[j][1]]+f[j][3])
return 有負環;
}
return 那沒事了;
而且還有小小的優化(不是SPFA)
for(int i=1;i<=n-1;i++){
bool chk=0;
for(int j=1;j<=m;j++){
if(dis[v[j]]>dis[u[j]]+w[j]){
chk=1;
dis[v[j]]=dis[u[j]]+w[j];
}
}
if(!chk)break;
}
//就是說,如果這一輪都一次都沒有鬆弛,那以後就更不可能去鬆弛了
(3)the dead SPFA
是貝爾曼的優化版,省去了無用點的鬆弛
時間複雜度:\(O(kn)\)
可以用來求最長路
禮貌拓撲:你嗎
模板:
while(!q.empty()){//當佇列非空 int u=q.front();//把隊首揪出來 q.pop(); vis[u]=0; for(int i=head[u];i!=0;i=edge[i].next){//列舉從i出發的每條邊 int v=edge[i].to;//終點 int w=edge[i].dis;//邊權 if(dis[v]>dis[u]+w){ dis[v]=dis[u]+w;//鬆弛 if(!vis[v]){ vis[v]=1; q.push(v);//入隊 } } } } }
注:據rings(巨佬%%%)所說,“會死的很慘”。所以有了全源最短路。(update on 2021/6/15)
(4)Dijkstra
注:不能處理負邊權(是建立在貪心基礎上的演算法)
樸素版時間複雜度:O(n^2)每次找到最近的點,因為它不可能被更短的路徑鬆弛了
堆優化
時間複雜度:O((n+m)logn)
思路:
“未知的最近的終點”
維護一個堆只用logn即可
就可以省下不少時間
1、建立小根堆
struct node
{
int dis;
int pos;
bool operator <( const node &x )const{
return x.dis<dis;
}
};
priority_queue<node> q;
Dijkstra
while(!q.empty()){//堆中有點
node tmp=q.top();q.pop();
int u=tmp.pos; //取堆頂。
if(vis[u])continue;vis[u]=1;
for(int i=head[u]; i!=0; i=edge[i].next){//從這個點開始鬆弛
int v=edge[i].to;
int w=edge[i].dis;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push((node){dis[v], v});//維護未知點的小根堆
}
}
}
}
(5)其它
1、這道題
正解:二分(限制可用點)+ 最短路
2、關於邊權與狀態轉移
這道題把沒毀壞的路標記為無窮大;
這道題貪一波就會發現不能走重複點,並且邊權為布林型
這道題由於邊權為手續費,狀態轉移很特殊,是dis[]*(1-r[][])
update on 2021/6/15:同餘最短路,差分約束,分層圖最短路,全源最短路
同餘最短路
以同餘為轉化手段,構建關於最短路的條件,解決特定情境下的問題。
例:跳樓機
乍一看,哦!這不是我可愛的完全揹包麼!
再一看,哦!這不是我可愛的MLE麼!
......
略一想,哦!該請出我們可愛的最短路了!
1.以同餘為轉化手段
下面設x(第一種操作跳的層數)為基準數。
設dis[i]表示僅用y、z兩種手段,跳到第( k x + i )層時(即模x為i的層數),所跳到的最低的層數。(顯然i:0 -> x-1)
注意理解,實在不懂的話往下看然後回來手模樣例
所以,最終的答案就是Σ( h - dis[i] ) / x + 1,(i:0 -> x-1 )&& ( dis[i] <= h )
下文有解釋,記得手模樣例
例:h=10 ; x=3 ; y=4 ; z=5 .
因為從第一層出發,所以dis[1]=1;
dis[0]=6,因為要想從第1層走到第k層,使(k%x0),至少需要跳z層到達第六層。
同理,dis[2]=5。
因為dis[1]=1,所有從1到h的樓層中,只要%x1的就都能到達。
因為dis[0]=6,所有從6到h的樓層中,只要%x0的就都能到達。
因為dis[2]=5,所有從5到h的樓層中,只要%x2的就都能到達。
對於每一項dis,對答案貢獻的樓層數是( h - dis[i] ) / x + 1。
不同dis[i]的貢獻不可能重複,因為同一個數%x只有同一個值。 廢話
注:為節省空間,選基準數時儘量選最小數。
2.構建關於最短路的條件
轉化,把不同的dis之間建邊,邊權即為每次跳的樓層數。
很好理解,放個程式碼一下就懂了
for(int i=0;i<x;i++){
//add(起,終,邊權);
add(i,(a[2]+i)%x,a[2]);
add(i,(a[3]+i)%x,a[3]);
}
然後跑最短路即可。
關於邊界:設初始值為初始值即可。
3.特定情境下的問題
有個公式的,這裡由於不會用LaTeX 懶 就不講了,參見墨墨的等式
差分約束
再咕一會
參見部落格
分層圖最短路
自己去看飛行路線的題解吧,懶得講了。
還有凍結、最優貿易、
回家的路。
注:回家的路:建邊時有個小小的思維點需要仔細考慮。
下面是抄來的題解。
Q:為什麼要用分層圖最短路?
A:在一個正常的圖上可以進行 k 次決策,對於每次決策,不影響圖的結構,隻影響目前的狀態或代價。一般將決策前的狀態和決策後的狀態之間連線一條權值為決策代價的邊,表示付出該代價後就可以轉換狀態了。
———原文連結
那麼接下來就是寫法&分析關鍵點。
1、寫法
1)真 · 建 k 層圖,直接跑最短路。(有手就行不長腦袋)(建邊時仔細分析層層關係即可)
有邊的兩個點,多建一條到下一層邊權為0的單向邊,如果走了這條邊就表示用了一次機會。
有N個點時,1n表示第一層,(1+n)(n+n)代表第二層,(1+2n)(n+2*n)代表第三層,(1+i*n)(n+in)代表第i+1層。
因為要建K+1層圖,陣列要開到n * ( k + 1),點的個數也為n * ( k + 1 ) 。
———原文連結
2)D · 分層圖 · P 寫法,省下建一堆邊的空間
據說,
我們把dis陣列和vis陣列多開一維記錄k次機會的資訊。
dis[ i ][ j ] 代表到達 i 用了 j 次免費機會的最小花費.
vis[ i ][ j ] 代表到達 i 用了 j 次免費機會的情況是否出現過.
更新的時候先更新同層之間(即花費免費機會相同)的最短路,然後更新從該層到下一層(即再花費一次免費機會)的最短路。———原文連結
對於每個點,可以用一個結構體存下:p和pos,表示第幾層的第幾個點。
至於鬆弛,基本是一樣的。
if(dis[v][p]>dis[u][p]+w){
dis[v][p]=dis[u][p]+w;
q.push((node){v,p,dis[v][p]});
}
if(p>=k)continue;
if(dis[v][p+1]>dis[u][p]+w/2){
dis[v][p+1]=dis[u][p]+w/2;
q.push((node){v,p+1,dis[v][p+1]});
}
2、關鍵
1)分析層與層關係。
如,對於飛行路線,要將每條邊的邊權變成0連向下一層;
對於凍結,要將每條邊的邊權變成一半連向下一層;
對於回家的路,要做“面的垂線”,邊權為一併且可以從第二層返回第一層。
2)明確最終答案與初始值。
最終答案:
對於最優貿易,答案為第三層與第一層任選;
對於其餘,答案均為所有層任選。
初始值:
有的為僅初始化第一層,有的初始化所有層。
全源最短路
來源於某巨佬的提醒。
至於做法,題解講得很清楚,這裡僅在大板塊下做一個梳理。
.
upd on 2021/8/4:分層圖同餘最短路
同餘最短路一般都是裸題,看出來演算法就基本上切掉了。。。
但是題目中有時會加上諸如 “選不超過c個物品” 的限制
這不就是分層圖嗎
我們按同餘最短路的套路建圖,再加一維表示用了幾個物品。
跑分層圖即可,一般不用建邊。