虚树学习笔记

虚树学习笔记

定义

虚树指的是不同于原树(我称之为实树)的重构树,使得在同样能够求解的情况下,整棵树的节点数更少,从而使得在存在多次询问时,一些复杂度关于树的节点数的树上算法能够在时限内求解。

常用场景

一般来说,虚树的使用场景比较单一,常见于在实树上存在一些特殊节点,并且答案与这些节点有关的题目,此时可以提炼出所有特殊节点重新构建一棵树。

构造

首先将所有的特殊点提取出,然后按 dfn 序排序,接着依次放入栈中,同时判断连续两个特殊点 lca 的位置关系,维护一个最右链,使得栈内所有元素连起来是虚树中的一条链,栈顶元素一定是一个特殊点,所有 dfn 序比栈顶元素小,且不在栈内的元素都已经加入到虚树当中。具体过程如下。

在每一次添加新的特殊点之后,它和栈顶的特殊点的 lca 和栈顶的两个元素的位置关系可能有下面 4 4 4 种情况:

  1. 新的特殊点和栈顶的特殊点的 lca 就是栈顶的特殊点。此时,最右链从原来的 top-1->top 变为 top-1->top->new,因此只需要加入新的节点即可。
  2. 新的特殊点和栈顶的特殊点的 lca 在两个栈顶元素之间。此时,最右链从原来的 top-1->top 变为了 top-1->lca->new,因此将边 lca->top 加入虚树中,top 出栈,并且将 lca 和 new 依次加入栈中。
  3. 新的特殊点和栈顶的特殊点的 lca 是次栈顶元素。此时,最右链从原来的 top-1->top 变为了 top-1->new,因此将 top 出栈,再将 new 加入栈中。
  4. 新的特殊点和栈顶的特殊点的 lca 在次栈顶元素上。此时,最右链从原来的 top-n->top-n+1->…->top-1->top 变为了 top-n->lca->new。可以利用循环性质,每一次去除一条边,在不满足此性质后,一定属于情况 1,2,3 中任何一种可直接修改的情况。

code

scanf("%d",&k);
for(int i=1;i<=k;i++){
    scanf("%d",&lst[i]);
    ask[lst[i]]=true;
}
sort(lst+1,lst+1+k,cmp);
stk[top=1]=lst[1];
for(int i=2;i<=k;i++){
    int lc=lca(stk[top],lst[i]);
    while(1){
        if(Dep[lc]>=Dep[stk[top-1]]){
            if(lc!=stk[top]){
                add_edge1(lc,stk[top]);
                if(lc!=stk[top-1])stk[top]=lc;
                else --top;
            }
            break;
        }else{
            --top;
            add_edge1(stk[top],stk[top+1]);
        }
    }
    stk[++top]=lst[i];
}
while(--top)add_edge1(stk[top],stk[top+1]);

一些问题

Q:当栈内只有 1 1 1 个点时,也能够完成正确的操作吗?

A:可以。利用数组模拟栈,当 top=1 时,top-1 意味着未被赋值的 0 0 0,而 0 0 0 的深度为 0 0 0,意味着此时情况一定属于情况 1,2,则不会出现问题。但当栈内没有元素时会发生数组越界,因此需要将第一个特殊节点直接入栈。

Q:为什么要将特殊节点按 dfn 序排序?

A:若不按 dfn 序排序,则新的特殊节点可能在最右链的左边,此时会导致求解出现问题导致循环陷入死循环然后 MLE(关于我怎么知道会 MLE,详见例题-P687.[bzoj2286] [Sdoi2011]消耗战)。

Q:虚树的节点不会过多吗?

A: 不会。按照构造原理来说,每两个相邻的特殊的节点的 lca 也会加入虚树,最多 k − 1 k-1 k1 个,再加上原有的 k k k 个特殊节点,最差情况下共有 2 k − 1 2k-1 2k1 个节点。

例题

P687. [bzoj2286] [Sdoi2011]消耗战

Solution1

看到题面,想到树形dp。考虑 f i f_i fi 表示为在节点 i i i 的子树内部所有的特殊节点都切断与 1 1 1 号节点连接的最小花费。当节点 i i i 不是特殊节点时可以把节点 i i i 和节点 1 1 1 切断连接,也可以把节点 i i i 子树内部所有的特殊节点和节点 i i i 切断连接。但当节点 i i i 是特殊节点时只能把节点 i i i 和节点 1 1 1 切断连接。复杂度为 O ( n m ) O(nm) O(nm)

Solution2

考虑到当一个子树内部完全没有特殊节点时该节点无用,而答案仅与 k k k 个特殊节点有关,因此可以利用虚树将树的节点数缩减至 2 k − 1 2k-1 2k1 个,再按同样的思路跑一遍树形dp,复杂度 O ( ∑ k ) O(\sum k) O(k)

code
#include <bits/stdc++.h>
using namespace std;

const int N=2.5e5;
int n,m,k,u,v,w,cnt=1,cnt1,top,tot;
int head[N+5],to[(N<<1)+5],nxt[(N<<1)+5],dis[(N<<1)+5],head1[N+5],to1[N+5],nxt1[N+5],stk[N+5],Dep[N+5],Siz[N+5],Son[N+5],Top[N+5],Faz[N+5],Dfn[N+5],lst[N+5];
long long Min[N+5];
bool ask[N+5];

void add_edge(int u,int v,int w){
	to[cnt]=v,dis[cnt]=w,nxt[cnt]=head[u];
	head[u]=cnt++;
	return ;
}

void add_edge1(int u,int v){
	to1[cnt1]=v,nxt1[cnt1]=head1[u];
	head1[u]=cnt1++;
	return ;
}

bool cmp(int a,int b){
	return Dfn[a]<Dfn[b];
}

long long min(long long a,long long b){
	return a<b?a:b;
}

void dfs(int u,int f){
	Siz[u]=1,Son[u]=0;
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i],w=dis[i];
		if(v==f)continue;
		Faz[v]=u,Dep[v]=Dep[u]+1,Min[v]=min(Min[u],w);
		dfs(v,u);
		Siz[u]+=Siz[v];
		if(Siz[v]>Siz[Son[u]])Son[u]=v;
	}
	return ;
}

void build(int u,int rt){
	Top[u]=rt,Dfn[u]=++tot;
	if(Son[u])build(Son[u],rt);
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==Faz[u]||v==Son[u])continue;
		build(v,v);
	}
}

int lca(int x,int y){
	while(Top[x]!=Top[y]){
		if(Dep[Top[x]]<=Dep[Top[y]])swap(x,y);
		x=Faz[Top[x]];
	}
	if(Dep[x]<Dep[y])swap(x,y);
	return y;
}

long long dfs1(int u){
	long long sum=0,res=0;
	for(int i=head1[u];i;i=nxt1[i]){
		int v=to1[i];
		sum+=dfs1(v);
	}
	if(ask[u])res=Min[u];
	else res=min(sum,Min[u]);
	ask[u]=false,head1[u]=0;
	return res;
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		add_edge(u,v,w),add_edge(v,u,w);
	}
	Min[1]=1e18,Dep[1]=1;
	dfs(1,0);
	build(1,1);
	scanf("%d",&m);
	while(m--){
		cnt1=1;
		scanf("%d",&k);
		for(int i=1;i<=k;i++){
			scanf("%d",&lst[i]);
			ask[lst[i]]=true;
		}
		sort(lst+1,lst+1+k,cmp);
		stk[top=1]=lst[1];
		for(int i=2;i<=k;i++){
			int lc=lca(stk[top],lst[i]);
			while(1){
				if(Dep[lc]>=Dep[stk[top-1]]){
					if(lc!=stk[top]){
						add_edge1(lc,stk[top]);
						if(lc!=stk[top-1])stk[top]=lc;
						else --top;
					}
					break;
				}else{
					--top;
					add_edge1(stk[top],stk[top+1]);
				}
			}
			stk[++top]=lst[i];
		}
		while(--top)add_edge1(stk[top],stk[top+1]);
		printf("%lld\n",dfs1(stk[1]));
	}
	return 0;
}
Some saying

不必多言

P2014. 「SDOI2015」寻宝游戏

Solution1

考虑到最终的答案就是一个包含所有特殊节点的极小联通子树的边权和的二倍,而这个数值和按照 dfn 序排序后,任意两个相邻特殊节点的距离的和相等(首尾也算相邻)。因此添加一个节点 y y y 后,如果它左边的节点为 x x x,右边的节点为 z z z,那么答案就增加 d i s ( x , y ) + d i s ( y , z ) − d i s ( x , z ) dis(x,y)+dis(y,z)-dis(x,z) dis(x,y)+dis(y,z)dis(x,z)。删去一个节点则正相反,考虑用 set 可以简单维护。

code
#include <bits/stdc++.h>
using namespace std;

const int N=1e5;
int n,m,x,y,z,t,pre,nxt;
long long ans;
bool vis[N+5];
struct R_Graph{
	int cnt;
	int head[N+5],v[(N<<1)+5],to[(N<<1)+5],w[(N<<1)+5];
	void clear(){
		cnt=1;
		return ;
	}
	void add_edge(int x,int y,int z){
		v[cnt]=y,w[cnt]=z,to[cnt]=head[x];
		head[x]=cnt++;
		return ;
	}
}RG;
struct LCA{
	int tot;
	int dep[N+5],siz[N+5],son[N+5],faz[N+5],dfn[N+5],top[N+5],rk[N+5];
	long long dis[N+5];
	void dfs1(int u){
		siz[u]=1,son[u]=0;
		for(int i=RG.head[u];i;i=RG.to[i]){
			int v=RG.v[i],w=RG.w[i];
			if(v==faz[u])continue;
			dep[v]=dep[u]+1,faz[v]=u,dis[v]=dis[u]+w;
			dfs1(v);
			siz[u]+=siz[v];
			if(siz[v]>siz[son[u]])son[u]=v;
		}
		return ;
	}
	void dfs2(int u,int rt){
		dfn[u]=++tot,top[u]=rt,rk[tot]=u;
		if(son[u])dfs2(son[u],rt);
		for(int i=RG.head[u];i;i=RG.to[i]){
			int v=RG.v[i];
			if(v==faz[u]||v==son[u])continue;
			dfs2(v,v);
		}
	}
	int lca(int x,int y){
		while(top[x]!=top[y]){
			if(dep[top[x]]<dep[top[y]])swap(x,y);
			x=faz[top[x]];
		}
		if(dep[x]<dep[y])swap(x,y);
		return y;
	}
	long long get_dis(int x,int y){
		return dis[x]+dis[y]-2*dis[lca(x,y)];
	}
}Lca;
set<int>s;
set<int>::iterator it;

int main(){
	RG.clear();
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		RG.add_edge(x,y,z),RG.add_edge(y,x,z);
	}
	Lca.dfs1(1),Lca.dfs2(1,1);
	for(int i=1;i<=m;i++){
		scanf("%d",&t);
		if(!vis[t])s.insert(Lca.dfn[t]);
		pre=Lca.rk[(it=s.lower_bound(Lca.dfn[t]))==s.begin()?*--s.end():*--it];
		nxt=Lca.rk[(it=s.upper_bound(Lca.dfn[t]))==s.end()?*s.begin():*it];
		if(vis[t])s.erase(Lca.dfn[t]);
		long long dis=Lca.get_dis(pre,t)+Lca.get_dis(t,nxt)-Lca.get_dis(pre,nxt);
		if(!vis[t]){
			vis[t]=true;
			ans+=dis;
		}else{
			vis[t]=false;
			ans-=dis;
		}
		printf("%lld\n",ans);
	}
}

P688. 「HEOI2014」大工程

Solution1

暴力枚举每两个节点,利用lca求解路径长度维护答案,复杂度 O ( ∑ k 2 log ⁡ k ) O(\sum k^2\log k) O(k2logk)

OJ和洛谷上 80 80 80 pts?这题有什么存在的意义?

Solution2

考虑需要求解的是树上的路径,自然地考虑到点分治,复杂度 O ( q n log ⁡ n ) O(qn\log n) O(qnlogn)

20 20 20 pts 直接拿下!

Solution 3

考虑 Solution2 相比于 Solution1 欠缺的是相关于 k k k 的时间复杂度,而又考虑到整个点分治需要求解的路径的端点仅与求解的 k k k 个点有关,因此可以建立 2 k 2k 2k 个点的虚树,从而复杂度为 O ( ∑ k log ⁡ k ) O(\sum k\log k) O(klogk)

终于拿下了!

code
#include <bits/stdc++.h>
using namespace std;

const int N=1e6;
int n,m;
struct Graph{
	int cnt,a,b;
	int head[N+5],to[(N<<1)+5],v[(N<<1)+5];
	void clear(){
		cnt=1;
		return ;
	}
	void add(int x,int y){
		v[cnt]=y;
		to[cnt]=head[x];
		head[x]=cnt++;
		return ;
	}
	void add_edge(){
		scanf("%d%d",&a,&b);
		add(a,b),add(b,a);
	}
}RG,IG;
struct LCA{
	int tot;
	int dep[N+5],siz[N+5],faz[N+5],son[N+5],top[N+5],dfn[N+5];
	void dfs1(int u){
		siz[u]=1;
		son[u]=0;
		for(int i=RG.head[u];i;i=RG.to[i]){
			int v=RG.v[i];
			if(v==faz[u])continue;
			faz[v]=u;
			dep[v]=dep[u]+1;
			dfs1(v);
			siz[u]+=siz[v];
			if(siz[v]>siz[son[u]])son[u]=v;
		}
		return ;
	} 
	void dfs2(int u,int rt){
		top[u]=rt;
		dfn[u]=++tot;
		if(son[u])dfs2(son[u],rt);
		for(int i=RG.head[u];i;i=RG.to[i]){
			int v=RG.v[i];
			if(v==faz[u]||v==son[u])continue;
			dfs2(v,v);
		}
		return ;
	}
	void pre(){
		dfs1(1);
		dfs2(1,1);
		return ;
	}
	int lca(int x,int y){
		while(top[x]!=top[y]){
			if(dep[top[x]]<dep[top[y]])swap(x,y);
			x=faz[top[x]];
		}
		if(dep[x]<dep[y])swap(x,y);
		return y;
	}
	int dis(int x,int y){
		return dep[x]+dep[y]-2*dep[lca(x,y)];
	}
}Lca;
bool cmp(int x,int y){
	return Lca.dfn[x]<Lca.dfn[y];
}
struct PointTree{
	int k,tot,root,top;
	int dis[N+5],siz[N+5],maxp[N+5],lst[N+5],stk[N+5];
	long long sum,Min,Max,pre,nxt,pre_min,pre_max,nxt_min,nxt_max,cnt;
	bool vis[N+5],ask[N+5];
	long long min(long long a,long long b){
		return a<b?a:b;
	}
	long long max(long long a,long long b){
		return a>b?a:b;
	}
	void get_root(int u,int fa,int total){
		siz[u]=1;
		maxp[u]=0;
		for(int i=IG.head[u];i;i=IG.to[i]){
			int v=IG.v[i];
			if(v==fa||vis[v])continue;
			get_root(v,u,total);
			siz[u]+=siz[v];
			maxp[u]=max(maxp[u],siz[v]);
		}
		maxp[u]=max(maxp[u],total-siz[u]);
		if(maxp[u]<maxp[root])root=u;
		return ;
	}
	void get_siz(int u,int fa){
		siz[u]=1;
		for(int i=IG.head[u];i;i=IG.to[i]){
			int v=IG.v[i];
			if(v==fa||vis[v])continue;
			get_siz(v,u);
			siz[u]+=siz[v];
		}
		return ;
	}
	void get_dis(int u,int fa,int d){
		if(ask[u]){
			dis[++tot]=d;
			nxt+=d;
			++cnt;
			nxt_min=min(nxt_min,d);
			nxt_max=max(nxt_max,d);
		}
		for(int i=IG.head[u];i;i=IG.to[i]){
			int v=IG.v[i];
			if(v==fa||vis[v])continue;
			get_dis(v,u,d+Lca.dis(u,v));
		}
		return ;
	}
	void calc(int u){
		tot=pre=0;
		pre_max=-2e9;
		pre_min=2e9;
		if(ask[u]){
			dis[++tot]=0;
			pre_max=pre_min=0;
		}
		for(int i=IG.head[u];i;i=IG.to[i]){
			int v=IG.v[i];
			if(vis[v])continue;
			nxt=cnt=0;
			nxt_max=-2e9;
			nxt_min=2e9;
			get_dis(v,u,Lca.dis(u,v));
			sum+=nxt*(tot-cnt)+pre*cnt;
			Min=min(Min,pre_min+nxt_min);
			Max=max(Max,pre_max+nxt_max);
			pre+=nxt;
			pre_max=max(pre_max,nxt_max);
			pre_min=min(pre_min,nxt_min);
		}
		return ;
	}
	void dfs(int u){
		vis[u]=true;
		calc(u);
		ask[u]=false;
		for(int i=IG.head[u];i;i=IG.to[i]){
			int v=IG.v[i];
			if(vis[v])continue;
			root=0;
			get_root(v,0,siz[v]);
			get_siz(v,0);
			dfs(root);
		}
		vis[u]=false;
		IG.head[u]=0;
		return ;
	}
	void query(){
		sum=Max=0;
		Min=2e9;
		IG.clear();
		scanf("%d",&k);
		for(int i=1;i<=k;i++){
			scanf("%d",&lst[i]);
			ask[lst[i]]=true;
		}
		sort(lst+1,lst+1+k,cmp);
		stk[top=1]=lst[1];
		for(int i=2;i<=k;i++){
			int lc=Lca.lca(lst[i],stk[top]);
			while(1){
				if(Lca.dep[lc]>=Lca.dep[stk[top-1]]){
					if(lc!=stk[top]){
						IG.add(lc,stk[top]);
						IG.add(stk[top],lc);
						if(lc!=stk[top-1])stk[top]=lc;
						else --top;
					}
					break;
				}else{
					--top;
					IG.add(stk[top],stk[top+1]);
					IG.add(stk[top+1],stk[top]);
				}
			}
			stk[++top]=lst[i];
		}
		while(--top){
			IG.add(stk[top],stk[top+1]);
			IG.add(stk[top+1],stk[top]);
		}
		maxp[0]=n;
		root=0;
		get_root(stk[1],0,n);
		get_siz(stk[1],0);
		dfs(root);
		printf("%lld %lld %lld\n",sum,Min,Max);
		return ;
	}
}PT;

int main(){
	RG.clear();
	scanf("%d",&n);
	for(int i=1;i<n;i++)RG.add_edge();
	Lca.pre();
	scanf("%d",&m);
	for(int i=1;i<=m;i++)PT.query();
}
More solutions

无可否认的,这道题选用点分治无疑是杀鸡用牛刀,但是谁叫我不会推树形dp呢?因此,这道题可以利用各种各样的树上算法,例如树形dp,最后利用虚树优化即可。

P2015. 「ZJOI2019」语言

九条可怜狗都不做,先颓着。

P686. 「HNOI2014」世界树

I’m now thinking aout it.

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值