tarjan2

反過來調過去,我還是感覺沒學明白縮點

  • 講一個有向圖中的所有強連通分量縮成一個點後,構成的新圖是一個DAG。
  • 一個點所在的強連通分量一定被該點所在DFS搜索樹所包含
  • 樹上的邊大致分為:樹枝邊,前向邊(從上往下指),後向邊(從下往上指),橫叉變。其中前向邊肉眼可見地沒什麼卵用

接下來開始演算法流程。

  • tarjan的精髓如上次所說,在於DFS搜索樹,在DFS搜索樹中強連通分量以怎樣形式存在是關鍵問題。對於x,存在祖宗y,從x出發可經過橫叉邊,返祖邊,後向邊到達y,則x,y屬於同一強連通分量。操作中記錄最小 y 為:low(x)=dfn(y)。(其中單點也算強連通塊)如果有一個dfn_x=low_x ,那麼就是說 x 在一個新的強連通塊里,同理,low_x的初始也就是dfn_x。
  • 我們用一個棧來維護 已經被遍歷過的、還未確定隸屬哪個強連通分量的 點,在該棧中越靠棧頂DFS序越靠後(是棧底元素的後代)。
  • 關於low_x的求法、更新。考慮如何求low_x:low_x 可能被更新,當且僅當x連出了一條樹枝邊,橫叉邊或後向邊。設該邊連向點 v

1.  樹枝邊: low_x= min(low_x,low_v) v 到達的點x一定可以到達,且v與x有祖宗關係
2.  後向邊: low_x= min(low_x,low_v) v 的祖先一定是 x 的祖先
3.  橫叉邊:此時分兩種情況考慮的
            當 v 點已經退棧時,那麼點v可到達的DFS序最小的祖先不是x的祖先,對 low_x 沒有貢獻;
當點v還在棧中時,v 點可到達的DFS序最小的祖先是x的祖先,有 low_x=min(low_x,low_v) (點v可到達的DFS序最小的祖先一定是x的,v 點能到達的點,x一定能到達)  特別地,由於前向邊的更新對於求強連分量沒有幫(更新是重複的),所以我們也可以有 low_x=min(low_x,low_v)

         那麼我們只需判斷點 x 連出的邊是哪一條就可以轉移了。顯然,當 dfn_v=0 時(此時v未被訪問過),這是一條樹枝邊。我們再維護一個 col 數組, col_i 表示點 i 所在的強連通分量,在點 i 退棧時,我們對col進行賦值,那麼當 dfn_v≠0&&col_v=0 時,點v一定在棧中(後向邊指向的點一定在棧中,橫叉邊指向的點滿足此條件時在棧中,而前向邊是否存在與答案無關),此時用 low_x=min(low_x,low_v) 轉移即可,否則無需轉移。該演算法時間複雜度為(n+m),因為深度優先遍歷每個點只會經過一次,每條邊也只會訪問一次,而每個點都只會進/出棧一次,所以總時間複雜度為(n+m)

//把一個點當成根提溜出來,抖摟抖摟成一棵樹 
void dfs(int u)
{
//記錄dfs序
//可通過任意多dfs邊與最多一條非樹返祖邊到達的、本強連通分量內最小點 
	dfn[u]=low[u]=++dfs_clock;
	s.push(u);
	for(int v:g[u])
	{
		if(!dfn[v])//樹邊 
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(!sccnum[v])//返祖 
			low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		scccnt++;//強連通塊+1 
		while(1)
		{
			int x=s.top();
			s.pop();
			sccnum[x]=scccnt;
			sccsz[scccnt]++;
			if(x==u) break;
		}
	}
}

 

Tags: