SPFA與(負)環-BFS與DFS
前言
一星期前用差分約束寫了獎學金這道題,在判斷impossible的時候,使用了spfa()常用的邊數判斷法,即更新一個點使用的邊數最多應當是n條,在其他點跑的飛快的情況下T了一個點,是一個有環的點,看別人的程式碼,有的是用dfs判環(這個也有坑下面說),還有的用了雙端佇列優化。看的我一愣一愣的。由於對spfa及其它判環的原理並不熟悉,我不知道我錯在哪裡,只有改成上述兩種方式才能過。於是我去洛谷尋找spfa判環的模板題,藉此瞭解spfa,在這道題中使用dfs竟然會TLE,難道dfs不是O(m)的嗎?什麼樣的構造會讓它Tle?這次的隨筆探討的就是這幾個問題:
1. spfa原理是什麼?複雜度上界怎麼來的?
2. spfa怎麼處理環?bfs的兩種實現,dfs的一種實現,有向圖和無向圖的區別?
關於spfa
spfa的原理
-
實現原理
spfa是佇列優化的bellman-ford,其實我感覺它就是個普通的bfs(別人的說法SPFA 在形式上和BFS非常類似,不同的是BFS中一個點出了佇列就不可能重新進入佇列,但是SPFA中一個點可能在出佇列之後再次被放入佇列,也就是一個點改進過其它的點之後,過了一段時間可能本身被改進,於是再次用來改進其它的點,這樣反覆迭代下去)
具體來說就是:從源點出發,遍歷所有出邊併入隊,再然後再遍歷佇列中的點,每次進行"鬆弛操作"(其實我覺得叫"更新操作"或者叫"糾正操作"更容易理解)
記每次隊頭的點為x,從對列中彈出隊頭x。
記dis[]為一個點到源點的最短/長路,y為出度點,即edge[i].to,求最短路的時候鬆弛的條件dis[y]>dis[x]+edge[i].cost,最長路的時候就是dis[y]<dis[x]+edge[i].cost;
當一個點滿足這個條件就該更新dis,或者叫糾正dis,如果這個點不在佇列中,就把它放入佇列。
重複以上步驟直到佇列為空。 -
正確性(邏輯原理):
只要聯通,bfs必定會遍歷全圖,每個點第一次被掃到必定更新,必定入隊。
佇列中每次都會保留待擴充套件的節點,如果存在dis還能更新,它就會一直更新下去,佇列就不會為空,只有所有點都完成更新,佇列才會空,保證了正確性。 -
複雜度:
運氣好的話,一次遍歷就把圖更新完了,所有點都不會再擴充套件,不需要再更新,比如鏈,或者說更新的比較少。
運氣一般的話,因為每次佇列中都存在待擴充套件節點,所以說隊頭出來之後如果掃到了佇列中的點,只可能更新dis而不會再把這個點多展開一次(dfs就可能搜完這次,回溯回來再搜一次),複雜度比dfs優。
(圖)運氣很不好,就會達到上界,跨入\(O(nm)\)
比如說用網格圖 來自洛谷某討論:網格圖,10行10000列,縱向邊權為1,橫向邊權隨機,親測普通spfa要跑1分鐘
非常可怕的複雜度。
所以對於正權圖,跑Dijkstra+heap,穩定log
spfa的環判定
- BFS
- 記錄入隊次數
用cnt記錄每個點入隊的次數,如果一個點入隊次數大於等於n次,就認為存在負環
為什麼?對於bfs,一次bfs中,沒有負環的情況下一個點最多入隊n次(從源點出連向所有其它點,而所有其他點又指向一個點,每個點正好將這個點更新一次,入隊一次),如果說還能再更新一次這個點,那一定是這個點出發跑了一個負環再到達自己,cnt在進入是否
入隊的判斷中執行。程式碼如下
- 記錄入隊次數
bool spfa(){
q.push(1);
vis[1]=1;
while(!q.empty()){
int x=q.front();q.pop();
vis[x]=0;
for(reg int i=head[x];i;i=edge[i].next){
int y=edge[i].to;
if(dis[y]>dis[x]+edge[i].cost){
dis[y]=dis[x]+edge[i].cost;
cnt[y]=cnt[x]+1;
if(cnt[y]>=n) return 1;//包含的邊數
if(!vis[y]){
cnt[y]++;
/*if(cnt[y]>=n) return 1; */
q.push(y);
vis[y]=1;
}
}
}
}
return 0;
}
- 記錄到達某個點使用的經過的邊數
我們可以判斷1號點到i號點的最短路徑長度是否<n(即經過的點數<=n,沒有任何一個點被重複經過)用cnt[y]=cnt[x]+1,cnt[y]>=n來判斷,只要進入鬆弛的判斷中就可以執行
程式碼如下
bool spfa(){
q.push(1);
vis[1]=1;
while(!q.empty()){
int x=q.front();q.pop();
vis[x]=0;
for(reg int i=head[x];i;i=edge[i].next){
int y=edge[i].to;
if(dis[y]>dis[x]+edge[i].cost){
dis[y]=dis[x]+edge[i].cost;
/*cnt[y]=cnt[x]+1;
if(cnt[y]>=n) return 1;//包含的邊數 */
if(!vis[y]){
cnt[y]++;
if(cnt[y]>=n) return 1;
q.push(y);
vis[y]=1;
}
}
}
}
return 0;
}
- DFS
dfs版本的思路是在一次搜尋中,一旦一個點更新了兩次,就說明有負環,為什麼?首先搜到一個點兩次肯定是有環的,然後第一次更新完了第二次又更新了,除非環的權值是負的,否則不可能更新,於是這樣就簡單的找到了負環。
對於dfs,有有向圖和無向圖的區別。如果是一個無向圖,vis直接標記,每個非標記點都可以進入,也不用回溯清零,一旦第二次掃描到某個點,就存在環。
如果是一個有向圖,掃完一個點的所有出邊需要把這個點的標記給去除,不然一個點出發兩條路從而到達一個點且無迴路,也會被認為是一個環。
dfs一般來說效率是很高的,不過特殊構造下還是會T。並且一次只能處理一個源點出發是否存在環,要求整個圖,需要掃描n個點。
程式碼如下
bool check(int k)
{
vis[k]=1;
for(int i=head[k];i;i=edge[i].next)
{
int y=edge[i].to;
if(dis[k]+edge[i].cost<dis[y]){
dis[y]=edge[i].cost+dis[k];
if(!vis[y])
{
if(check(y))//前面搜到了負環
return 1;
}
else return 1;//一次深搜中搜了一個點兩次,則存在負環。
}
}
vis[k]=0;
return 0;
}
二者的取捨
用差分約束並且用bfs判負環導致獎學金這道題T了之後:這判負環必用dfs啊!
用dfs做了洛谷P3385 判負環 之後:這dfs有問題啊。
怎麼說呢,如果題目說資料隨機,但是可能有環,用dfs判一下環顯然會更快,很多時候都是\(O(m)\)的。
如果說沒有說資料隨機,有機率卡dfs。
如果說題目一看就是得用spfa的,說是有資料存在負環的,那一般不會卡spfa了,但是獎學金這道題就是卡了,不過用了雙端佇列也就是SLF優化之後就會快很多,直接過了。
總結來說,實際上dfs用的時候需要謹慎,確定有負環的情況下SLF優化還是蠻必要的,這樣才更有保障。
另外肯定不能用dfs求最短路,那跟暴力沒區別。
如果圖沒有負權邊,那妥妥的Dijkstra+heap。