1. 程式人生 > 其它 >【模板】支配樹

【模板】支配樹

看了許多大佬的講解,就用自己的語言說一遍,可能講得不好(畢竟還是太菜了)

題目大醫

給定一張有向圖,求從起點出發,求所有點再去掉的情況下有多少個點到達不了(就是支配點的概念)。

\(n\leq 2\cdot10^5\)\(m\leq 3\cdot 10^5\)

前肢假想

根據題目,我們以此來建一個樹,稱之為“支配樹”

滿足“支配樹”要滿足這些性質:

1.這是一個樹(而不是圖),根節點就是起點;

2.對於任意點 \(u\) ,從根到它的簡單路徑上的所有點都是它的支配點;

3.對於任意點 \(u\) ,它支配它的子樹上所有的子孫節點。

如果能這樣造出來,問題就解決了。

前肢引入

\(Lengauer-Tarjan\)

演算法

哪裡都有 Tarjan 老爺子。。。 Tarjan orz !

\(Lengauer-Tarjan\) 演算法主要分了三個步驟:

1.從起點開始造 \(dfs\) 樹並求出所有點的 \(dfn\)

2.求出所有點的半支配點;(暫設陣列 \(semi(i)\)

3.求出所有點的支配點。(暫設陣列 \(idom(i)\)

其中半支配點是一個新概念,也是間接求支配點的精髓。

對於任意點 \(u\) 的半支配點表示所有點中有 dfn 最小的點 \(v\) ,能使 \(v\) 滿足有一條路徑能夠到達 u 且路徑上的所有點( \(u\)\(v\) 都不算)的 \(dfn\) 都大於 \(dfn(u)\)

豬蹄內容

就只講講一般有向圖吧,因為這個解決了其他的就都解決了(如樹, DAG )。

綢之路的誕生之半支配點

設目前我們在研究點 \(u\) ,那麼再設點 \(v\) 指所有直接連向 \(u\) 的點。

分類討論(王道!)

1.\(dfn(v)=dfn(u)\)

直接無解。。跳過。

2.\(dfn(v)<dfn(u)\)

如果要從這條路徑上走的話,剛開始就結束了(根據定義來),所以把 \(v\) 和可能存在的 \(semi(u)\) 擇優更新就好了。

3.\(dfn(v)>dfn(u)\)

那麼就在 \(dfs\) 樹上從 \(v\) 找一個祖先 \(w\)\(dfn(w)>dfn(u)\)

,然後就把可能存在的 \(semi(w)\) 和可能存在的 \(semi(u)\) 擇優更新。

因為既然有了 \(semi(w)\) ,這一段路徑上所有 \(dfn\) 都大於 \(dfn(u)\) (定義來的), \(w\) 又是從 \(semi(u)\)\(u\) 的路徑上搜到的,所以 \(w\) 之後的點也滿足。

(不是很懂??正常,我也有點雲裡霧裡的(就算明白了,一會也就忘了),但關鍵好背!!)

至此,半支配點完結!

綢之路的誕生之支配點

考慮在半支配點的肩膀上求出支配點。

設目前我們在研究點 \(u\)

然後記一個點集 \(\large\xi\) 表示在 \(dfs\) 樹上從 \(semi(u)\)\(u\) 的路徑的點集,(但不包括 \(semi(u)\) )其中點 \(\alpha\)\(\large\xi\)\(dfn(semi)\) 最小的點。

那麼,分類討論(王道!)

(肯定是判斷 \(semi\) 相等而不是比較 \(dfn\) 大小了呀)

1.\(semi(\alpha)=semi(u)\)

\(idom(u)=semi(u)\)

因為 \(\alpha\) 已經是 \(semi(u)\)\(u\) 路徑中所有點的下限( \(dfn(semi)\) )了,如若當前情況就是 \(\large\xi\) 成了個封閉的點集, \(semi(u)\) 是唯一直接相連的點,自然就有了 \(idom(u)=semi(u)\)

2.\(semi(\alpha)\neq semi(u)\)

\(idom(u)=idom(\alpha)\)

感性理解:

倘若有這麼一個“中間點” \(\beta\)

我們假設沒有了 \(idom(\alpha)\)\(u\) 還可以能夠到達。那麼說明 \(idom(\alpha)\) 祖先會有一個點有一條非 \(dfs\) 樹邊連向了 \(idom(\alpha)\)\(u\) 中間的一個點 \(\beta\)

其中, \(\alpha\) 必是 \(\beta\) 的祖先,否則沒有了 \(idom(\alpha)\) 也能到達 \(\alpha\) ,顯然與定義不符。

如若如此, \(\beta\) 便一定在 \(\large\xi\) 中了。

看得出來, \(semi\) 的要求比 \(idom\) 高一些,從滿足條件的難度上,所以 \(dfn(idom(\alpha))\leq dfn(semi(\alpha))\) 顯然。(記住,這是感性。。)

\(idom(\alpha)\) 的祖先有一條非 \(dfs\) 樹邊連向了 \(\beta\) ,所以無論如何 \(dfn(semi(\beta))<dfn(idom(\alpha))\)

結合上面的兩段內容,發現會與 \(\large\xi\) 關於 \(\alpha\) 的定義不符了。所以沒有了 \(idom(\alpha)\)\(u\) 一定不能夠到達。

至此,支配點完結!

從絲綢之路到實際實現

1.半支配點

如果還有點印象的話,應該能知道求解過程都是 dfn 從大到小的逆推,所以可以倒序 dfn 處理。

對於第二個情況(第一個就。。)直接幹,而第三種情況,肯定是不會暴力列舉的。

於是考慮一個帶權並查集(說的好像要怎麼樣,但程式碼就是好背),每次處理完一個點,就往它在 dfs 樹上的點合併,按照支配點中 \(\large\xi\) 的要求維護對應的東西。

2.支配點

很多輔助東西都與半支配點一併整好了,只需要把已經求的 semi 與對應的點連邊,構造半支配樹,就能同時求解支配點。

由於可能存在搜尋的時候 idom 並未找到,可以先記錄下來,最後順序 dfn 再額外處理。

有一個小注意可見這位大佬在程式碼上方的講述

時間複雜度 \(O(n\log_{2}{n})\) ,優雅的詮釋了\(Lengauer-Tarjan\) 演算法的優越性。

(沒辦法, Tarjan 老爺子就這麼厲害。。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10,M=3e5+10;
int n,m,ans[N],tot;
int fst[N][3],nxt[M+M+N],to[M+M+N];
int dfn[N],ord[N],cnt,fth[N];
int idom[N],semi[N],uni[N],mn[N];
inline ll read()
{
	ll s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
inline void add(int u,int v,int id)
{
	nxt[++tot]=fst[u][id];
	to[tot]=v,fst[u][id]=tot;
}
inline void Tarjan(int u)
{
	ord[dfn[u]=++cnt]=u;
	for(int i=fst[u][0];i;i=nxt[i])
	{
		int v=to[i];
		if(!dfn[v])
		{
			fth[v]=u;
			Tarjan(v);
		}
	}
}
inline int uni_query(int u)
{
	if(u==uni[u])return u;
	int tmp=uni_query(uni[u]);
	if(dfn[semi[mn[u]]]>dfn[semi[mn[uni[u]]]])mn[u]=mn[uni[u]];
	return uni[u]=tmp;
}
inline void Lengauer_Tarjan(int s)
{
	Tarjan(s);
	for(int i=1;i<=n;++i)semi[i]=uni[i]=mn[i]=i;
	for(int id=cnt;id>=2;--id)
	{
		int u=ord[id];
		for(int i=fst[u][1];i;i=nxt[i])
		{
			int v=to[i];
			if(!dfn[v])continue;
			uni_query(v);
			if(dfn[semi[u]]>dfn[semi[mn[v]]])semi[u]=semi[mn[v]];
		}
		uni[u]=fth[u];
		add(semi[u],u,2);
		u=fth[u];
		for(int i=fst[u][2];i;i=nxt[i])
		{
			int v=to[i];
			uni_query(v);
			idom[v]=(u==semi[mn[v]]?u:mn[v]);
		}
		fst[u][2]=0;
	}
	for(int i=2;i<=cnt;++i)
	{
		int u=ord[i];
		if(idom[u]^semi[u])
		idom[u]=idom[idom[u]];
	}
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=m;++i)
	{
		int x=read(),y=read();
		add(x,y,0),add(y,x,1);
	}
	Lengauer_Tarjan(1);
	for(int i=cnt;i>=2;--i)ans[idom[ord[i]]]+=(++ans[ord[i]]);
	++ans[1];
	for(int i=1;i<=n;++i)printf("%d ",ans[i]);
	return 0;
}

完結撒花!