1. 程式人生 > 實用技巧 >SPFA與(負)環-BFS與DFS

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。