淺談虛樹


在閱讀本文之前,你需要了解DFS序,樹鏈剖分算法與LCA.

Part1:虛樹的概念

虛樹,是對於一棵給定節點數\(n\)的樹\(T\),構造一棵新的樹\(T'\)使得節點總數最小且包含指定的某幾個節點和它們的LCA.

利用虛樹,可以對於指定多組點集\(S\)的詢問進行每組\(O(|S|\log n+f(|S|))\)的回答,其中\(f(x)\)指的是對於樹上\(x\)個點的情況下單組詢問這個問題的時間復雜度.可以看到,這個復雜度基本上(除了那個\(\log n\)以外)與\(n\)無關了.這樣,對於多組詢問的回答就可以省去每次詢問都遍歷一整棵樹的\(O(n)\)復雜度了.

Part2:虛樹的構造

我們以CF613D Kingdom and its Cities作為例題.

題意:

給定一棵樹,多組詢問,每組詢問給定\(k\)個點,你可以刪掉不同於那\(k\)個點的\(m\)個點,使得這\(k\)個點兩兩不連通,要求最小化\(m\),如果不可能輸出\(-1\).詢問之間獨立.數據范圍\(n\leq10^5,\sum k\leq10^5\).

一看到這種\(\sum k\leq10^5\)的題很可能就是虛樹了.

構造方法

先預處理整棵樹的LCA與DFS序,接下來是對於每組詢問的構造.

虛樹的構造是一個增量算法,要首先將指定的這\(k\)個點按照DFS序排序,然后按照順序一一加入.可以強行先加入根節點以方便后面的處理.

虛樹構建時會開一個棧\(S\),這個棧本質上和DFS遞歸時系統自動開的棧原理是一樣的.也就是說,這個棧保存了從根出發的一條路徑(按照深度從小到大存儲).當加入第\(k\)個指定的節點\(a_k\)后,滿足\(S[1]=root,S.top()=a_k,stk[x]\)\(S[x-1]\)的后代.虛樹上\((u,v)\)的邊的連接時間就是\(v\)被彈出棧的時間.

考慮如何加入一個新的節點\(x\).設\(z\leftarrow \text{LCA}(x,S.top())\),分兩類討論:

1.\(z=S.top()\),也就是\(x\)\(S.top()\)的子樹內節點.這時直接將\(x\)加入棧中即可.

2.\(z\ne S.top()\),這種情況中,\(x\)一定不是\(S.top()\)子樹內的節點.

Kq7Sht.md.png

這時原樹上的情況.這時,"..."代指的那些路徑上的節點以及\(S[S.size()-1],S.top()\)都應彈出棧外(相當於開始回溯,訪問\(z\)的另一棵子樹).注意,此時\(S[S.size()-1],S.top()\)\(z,x\)在樹上不一定直接相連,圖中只是少畫了幾個節點而已.

我們不斷彈出\(S.top()\),直到\(S[S.size()-1].dep<z.dep\),這時"..."表示的點全部出棧.在彈\(S.top()\)時都要在虛樹上連一條\((S[S.size()-1],S.top())\)的邊.

注意彈完時可能\(S.top()\ne z\),我們需要把\(z\)補充進虛樹來維護這個,直接加進棧即可.插入完所有點之后要完全回溯,也就是把棧內節點都彈出,也要連\((S[S.size()-1],S.top())\)的邊.

偽代碼如下:

\(\text{INSERT_TO_VIRTUAL_TREE}(x):\)
\(\mathbf{if}\ S.empty():\)
\(\quad S.push(x)\)
\(\quad \mathbf{return}\)
\(ancestor\leftarrow \text{LCA}(S.top(),x)\)
\(\mathbf{while}\ S.size()>1\ \mathbf{and}\ ancestor.dep<S[S.size()-1].dep\)
\(\quad \text{ADD_EDGE}(S[S.size()-1],S.top())\)
\(\quad S.pop()\)
\(\mathbf{if}\ ancestor.dep<S.top().dep\)
\(\quad \text{ADD_EDGE}(ancestor,S.top())\)
\(\quad S.pop()\)
\(\mathbf{if}\ S.empty()\ \mathbf{or}\ S.top()\ne ancestor\)
\(\quad S.push(ancestor)\)
\(S.push(x)\)

C++實現如下:

inline void insert(int x) // x為節點的編號
{
    if(top==0) // 用數組stk模擬棧,top為棧頂指針
    {
        stk[top=1]=x;
        return;
    }

    int ancestor=LCA(stk[top],x);

    while((top>1)&&(dep[ancestor]<dep[stk[top-1]]))
    {
        add_edge(stk[top-1],stk[top]);
        --top;
    }

    if(dep[ancestor]<dep[stk[top]])
        add_edge(ancestor,stk[top--]);

    if((!top)||(stk[top]!=ancestor))
        stk[++top]=ancestor;

    stk[++top]=x;
}

正確性

對於任意指定兩點\(a,b\)的LCA,都存在DFS序連續的兩點\(u,v(u.dfn\le v.dfn)\),分別屬於LCA包含\(a,b\)的兩棵子樹,此時\(v\)加入時的操作必定會使LCA入棧,所以所要求加入的點必然都加入了.對於非LCA點,按照上面的操作是不會出現這個點的,所以樹的大小是最小的.

復雜度

由於每個指定點進棧出棧各一次,這部分復雜度為\(O(\sum k)\).排序和求LCA的復雜度為\(O(\sum k\log k)\).

構建完成后的應用

以例題為例介紹虛樹的使用.首先特判掉無解情況(即一個點和他的父親都被指定).構造好虛樹后,我們給真正被指定的點的節點個數\(siz\)設置成\(1\)(因為有一些加入的點實際上只是LCA,要區分開來),然后DFS這棵虛樹.

以下所說的節點\(u\)可達是指有一個指定節點可以到達\(u\)(在執行了下面的刪點之后)

對於一個被指定的點\(u\),如果存在孩子\(v\)可達,那么意味着\(u,v\)不刪點就出現了連通,所以\(u,v\)上隨便去掉一個點就可以了(這里要\(ans\leftarrow ans+1\)),如果孩子不可達,那就不用處理了.

對於一個未被指定的點\(u\),統計有多少個孩子\(v\)可達.如果只有一個,把\(u\)設置成可達的就好了(相當於看上面的情況決定是否處理).如果超過一個,那把\(u\)刪掉就好了(這里也要\(ans\leftarrow ans+1\)).

單個詢問的偽代碼如下:

\(//ans\text{ is a global variable to store the result}\)
\(\text{VIRTUAL_TREE_DFS}(x):\)
\(\mathbf{if}\ x.size\ne 0\)
\(\quad\mathbf{for}\ each\ e\ \mathbf{in}\ x.adjacency:\)
\(\quad\quad\text{VIRTUAL_TREE_DFS}(e.to)\)
\(\quad\quad\mathbf{if}\ e.to.size\ne 0:\)
\(\quad\quad\quad e.to.size\leftarrow 0\)
\(\quad\quad\quad ans\leftarrow ans+1\)
\(\mathbf{else}:\)
\(\quad\mathbf{for}\ each\ e\ \mathbf{in}\ x.adjacency:\)
\(\quad\quad \text{VIRTUAL_TREE_DFS}(e.to)\)
\(\quad\quad x.size\leftarrow x.size+e.to.size\)
\(\quad\quad e.to.size\leftarrow 0\)
\(\quad\mathbf{if}\ x.size>1:\)
\(\quad\quad ans\leftarrow ans+1\)
\(\quad\quad x.size\leftarrow 0\)
\(x.adjacency.clear()\)

事實上難點完全在於建虛樹.

算法總復雜度\(O(n+\sum k\log k)\),如果用非\(O(1)\)的LCA要多一個\(\sum k\log n\),如果用倍增ST表LCA要多一個\(n\log n\).

注意在最后一遍DFS虛樹時,要把邊清空,具體只需要修改頭指針(對於vector直接erase)就可以了.如果對每個詢問暴力memset會導致復雜度退化為\(O(nq)\).

完整C++代碼如下:

const int Maxn=1e5+7,Maxm=2e5+7;

struct Edge
{
	int nxt,to;	
}e[Maxm];

int a[Maxn],head[Maxn],dfn[Maxn],top[Maxn],son[Maxn],siz[Maxn],f[Maxn],dep[Maxn];
int stk[Maxn];
int n,m,q,tp,cnt,ans;

/*
樹鏈剖分部分
*/

inline void add_edge(int x,int y)
{
	e[++cnt].nxt=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}

inline void DFS1(int x)
{
	siz[x]=1;
	
	for(int i=head[x];i;i=e[i].nxt)
	{
		int u=e[i].to;
		
		if(u==f[x])
			continue;
		
		dep[u]=dep[x]+1;
		f[u]=x;
		DFS1(u);
		siz[x]+=siz[u];
		
		if(son[x]==0||siz[son[x]]<siz[u])
			son[x]=u;
	}
}

inline void DFS2(int x)
{
	dfn[x]=++cnt;
	
	if(son[x])
	{
		top[son[x]]=top[x];
		DFS2(son[x]);
		
		for(int i=head[x];i;i=e[i].nxt)
			if((e[i].to!=f[x])&&(e[i].to!=son[x]))
				DFS2(top[e[i].to]=e[i].to);
	}
}

inline int LCA(int x,int y)
{
	while(top[x]!=top[y])
		if(dep[top[x]]>dep[top[y]])
			x=f[top[x]];
		else
			y=f[top[y]];
	
	return dep[x]<dep[y]?x:y;
}

/*
樹鏈剖分部分完
*/

inline void quick_sort(int l,int r)
{
	int i=l,j=r,mid=dfn[a[l+r>>1]];
	
	while(i<=j)
	{
		while(dfn[a[i]]<mid)
			++i;
		while(dfn[a[j]]>mid)
			--j;
		
		if(i<=j)
			swap(a[i++],a[j--]);
	}
	
	if(i<r)
		quick_sort(i,r);
	if(l<j)
		quick_sort(l,j);
}

inline void insert(int x)
{
	if(tp==0)
	{
		stk[tp=1]=x;
		return;
	}
	
	int ance=LCA(stk[tp],x);
	
	while((tp>1)&&(dep[ance]<dep[stk[tp-1]]))
	{
		add_edge(stk[tp-1],stk[tp]);
		--tp;
	}
	
	if(dep[ance]<dep[stk[tp]])
		add_edge(ance,stk[tp--]);
	
	if((!tp)||(stk[tp]!=ance))
		stk[++tp]=ance;
	
	stk[++tp]=x;
}

inline void DFS3(int x)
{
	if(siz[x])
		for(int i=head[x];i;i=e[i].nxt)
		{
			int u=e[i].to;
			DFS3(u);
			
			if(siz[u])
			{
				siz[u]=0;
				++ans;
			}
		}
	else
	{
		for(int i=head[x];i;i=e[i].nxt)
		{
			int u=e[i].to;
			DFS3(u);
			siz[x]+=siz[u];
			siz[u]=0;	
		}	
		
		if(siz[x]>1)
		{
			++ans;
			siz[x]=0;
		} 
	}
	
	head[x]=0;
}

int main()
{
	scanf("%d",&n);
	
	for(int i=1,x,y;i<=n-1;++i)
	{
		scanf("%d%d",&x,&y);
		add_edge(x,y);
		add_edge(y,x);
	}
	
	cnt=0;
	DFS1(dep[1]=1);
	DFS2(top[1]=1);
	memset(head+1,0,n<<2);
	memset(siz+1,0,n<<2);
	scanf("%d",&q);
	cnt=0;
	
	for(;q--;)
	{
		int x=1;
		scanf("%d",&m);
		
		for(int i=1;i<=m;++i)
		{
			scanf("%d",a+i);
			siz[a[i]]=1;
		}
		
		for(int i=1;i<=m;++i)
			if(siz[f[a[i]]])
			{
				puts("-1");
				x=0;
				break;
			}	
			
		if(!x)
		{
			while(m)
				siz[a[m--]]=0;
			
			continue;
		}
		
		ans=0;
		quick_sort(1,m);
			
		if(a[1]!=1)
			stk[tp=1]=1;
			
		for(int i=1;i<=m;++i)
			insert(a[i]);
		
		if(tp)
			while(--tp)
				add_edge(stk[tp],stk[tp+1]); // 回溯過程
			
		DFS3(1);
		siz[1]=cnt=0;
		printf("%d\n",ans);
	} 
}

本文完


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM