浅谈虚树(虚仙人掌)

虚树是什么?

在 OI 比赛中,有这样一类题目:给定一棵树,另有多次询问,每个询问给定一些关键点,需要求这些关键点之间的某些信息。询问数可能很多,但满足所有询问中关键点数量的总和比较小。

由于询问数可以非常多,每次无法遍历整棵树。 我们可以用一种叫做虚树(virtual tree)的魔法来解决这一问题。

一般来说,虚树有以下一些性质: 1、虚树的大小与点集同阶 2、如果u,v两点在虚树中,则她们的LCA也在虚树中。

于是,只要把握了虚树,就把握了这个点集的基本形态。

##虚树的构造 其实很朴素

普通的构造 1、将点集按DFS序排序。 2、排序后,求相邻两点的LCA。 3、再排序一次,并去重。 这个时候,虚树上该有的点都在集合中上了。 4、建树,用一个栈来模拟。 //既然已经有了DFS序,我觉得你应该也会建树了。

int dfn[M],lim[M],stk[M],vfa[M];
//dfn:dfs序,lim:子树的dfs序的最大值,vfa:虚树上的父节点
bool cmp(int a,int b){return dfn[a]<dfn[b];}
void vbuild(int array[],int len){//对array数组构建虚树,注意array须两倍len长
	int top=0,vn;
    sort(array,array+len,cmp); //使结点按照dfs序顺序有序 
    for(int i=1;i<len;++i)
        array[++vn]=Lca(array[i-1],array[i]); //处理出相邻结点的Lca 
    sort(array,array+vn,cmp); //按dfs序再有序 
	
    stk[top++]=array[0];
    for(int i=1;i<vn;++i){
        if(array[i-1]==array[i])continue;
        while(lim[stk[top-1]]<dfn[u]) --stop; //不是祖先结点
        vfa[array[i]]=stk[stop-1];
        stk[stop++]=array[i];
    }
}

线性构造法 其实吧,排序可以用基数排序,LCA也可以O(1)求,这样就线性了。

也可以用简单一点的方法,有兴趣的同学可以去学习一下。 

##例题 好像,虚树题目都挺朴素的,不会复杂到哪里去。 要出难题的话,要么在点集的产生方式上做文章~~,要么上仙人掌~~。 ###机房网络 从这里讲起吧。 看到互质,先来一波莫比乌斯反演压压惊。 定义f(k)是权值被k整除的点集的中的点两两之间的距离之和。 定义F(k)是最大公约数为k的点集的中的点两两之间的距离之和。 显然,有

根据莫比乌斯反演定理,可以得到:

所求即为F(1)

于是,可以先筛出莫比乌斯函数,再求f(n) 考虑怎么求f(n) 直接枚举权值为n的倍数的点再建虚树就可以了。 问题转化为,求树上任意两点的距离值和。

最后一个问题,任意两点的距离和怎么求? 考虑每条边的贡献 如果有一条从u连向u的父节点的边,那么经过这条边的路径数就是

我觉得已经够清楚的了。

贴代码:


long long calc(int k){
    int tot=0,n=0,top=0;
    for(int i=k;i<M;i+=k) FOR(j,0,E[i].size()) arr[n++]=E[i][j];
    sort(arr,arr+n,cmp);
    FOR(i,1,n) arr[n++]=LCA(arr[i-1],arr[i]);
    sort(arr,arr+n,cmp);
    n=unique(arr,arr+n)-arr;
    FOR(i,0,n){
        int u=arr[i];
        while(top and lim[stk[top-1]]<=dfn[u]) --top;
        if(top) vFa[u]=stk[top-1];
        stk[top++]=u;
    }//构造虚树
    long long res=0;
    FOR(i,0,n){
        Sz[arr[i]]=(A[arr[i]]%k==0);//只有权值被k整除的点才能产生贡献
        tot+=Sz[arr[i]];
    }
    DOR(i,n,1){
        int u=arr[i],v=vFa[u];
        res+=(long long)(dis[u]-dis[v])*Sz[u]*(tot-Sz[u]);//考虑每条边对答案的贡献
        Sz[v]+=Sz[u];
    }
    return res;
}
long long work(){
	long long ans=0;
	FOR(i,mu[1]=1,M) if(mu[i]){
        for(int j=i+i;j<M;j+=i) mu[j]-=mu[i];
        ans+=calc(i)*mu[i];
    }
	return ans;
}

复杂度分析:一个点被计入虚树的次数是其因子中的平方自由数的个数。 在100000以内,一个数最多只有64个平方自由数因子, 因此复杂度是O(64n log n)或者O(64n)

###[Bzoj3611][Heoi2014]大工程 这个做虚树入门题挺不错的。 直接造虚树,树形DP该怎么写就怎么写。

###开店 其实这是一个点分治裸题 不过Komachi还是用虚树写出来了,你可以问问她哦。 主要的思路是用线段树维护每个区间的虚树。 然后就可以在O(log n)个虚树上查询了。 查询的时候,可以对dfs序进行分类讨论。 代码总共只有6K,还是比较精炼的。

#ova:虚仙人掌 没什么不一样的。 把圆方树的虚树造出来就可以了。 只是有一点细节,要把方点直接连接的子节点都加入到虚树中的。 虚树大小还是与给出的点集同阶的。

##火车司机出秦川

先做一个圆方树的剖分。 用BIT维护跟到每个点的最长路径,最短路径,全部路径的权值和。 另外用BIT维护每个环的长度。 当LCA是一个环时,路径对环的贡献是环上的一段区间。 对每个环单独求贡献。 这个用括号序列,差分,BIT就可以解决。 时间复杂度是O(n log n + sum k_i log n)

//我写过的最长代码最长的题

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

#define FOR(a,b,c) for(int a=(b),a##_end__=(c);a<a##_end__;++a)
#define DOR(a,b,c) for(int a=(b)-1,a##_end__=(c);a>=a##_end__;--a)
template<class T>inline bool chkmin(T&a,T const&b){return a>b?a=b,true:false;}
template<class T>inline bool chkmax(T&a,T const&b){return a<b?a=b,true:false;}
const int M=600005;
long long Pool[M<<2],*allc=Pool;
struct BIT{
	long long *bit;
	int sz;
	void New(int x){bit=allc,allc+=sz=x;}
	void add(int i,int x){
		while(i<sz) bit[i]+=x,i+=i&-i;
	}
	long long sum(int i){
		long long res=0;
		while(i) res+=bit[i],i^=i&-i;
		return res;
	}
	void update(int l,int r,int x){
		add(l,x),add(r+1,-x);
	}
}f,g,h,ring[M];
vector<int>E[M],W[M],Q[M];
int low[M],dfn[M],Fa[M],Len[M],Rank[M],dep[M];
int Son[M],Sz[M],Top[M],id[M],lim[M],Node[M];
int Head[M],Next[M],to[M],vsz[M],sum[M][2];
int X[M],Y[M],type[M],U[M],V[M],C[M],px[M],py[M];
int n,m,q,dfn_cnt,id_cnt,bcc_cnt;

void Tarjan(int u,int fa){
	dfn[u]=low[u]=++dfn_cnt;
	for(auto v:E[u]) if(v!=fa){
		if(!dfn[v]){
			dep[v]=dep[u]+1;
			Tarjan(v,Fa[v]=u);
			chkmin(low[u],low[v]);
			if(low[v]>dfn[u]) W[u].push_back(v);
		}else chkmin(low[u],dfn[v]);
	}
	for(auto v:E[u]) if(dfn[v]>dfn[u] && Fa[v]!=u){
		int w=++bcc_cnt;
		Len[w]=dep[v]-dep[u]+1;
		for(int z=v;z!=u;z=Fa[z]){
			Rank[z]=dep[z]-dep[u];
			W[w].push_back(z);
		}
		reverse(W[w].begin(),W[w].end());
		ring[w].New(Len[w]+1);
		W[u].push_back(w);
	}
}
void Get_son(int u){
	Sz[u]=1;
	dfn[u]=++dfn_cnt;
	for(auto v:W[u]){
		dep[v]=dep[Fa[v]=u]+1;
		Get_son(v);
		Sz[u]+=Sz[v];
		if(Sz[v]>Sz[Son[u]]) Son[u]=v;
	}
	lim[u]=dfn_cnt;
}
void Get_top(int u,int p){
	Top[u]=p;
	Node[id[u]=++id_cnt]=u;
	if(Son[u]) Get_top(Son[u],p);
	for(auto v:W[u]) if(!Top[v]) Get_top(v,v);
}
int LCA(int u,int v){
	while(Top[u]!=Top[v])
		id[u]<id[v]?(v=Fa[Top[v]]):(u=Fa[Top[u]]);
	return id[u]<id[v]?u:v;
}
int UP(int u,int d){
	while(dep[Top[u]]>d) u=Fa[Top[u]];
	return Node[id[u]+d-dep[u]];
}
void update(int u,int v,int x){
	if(dep[u]<dep[v]) swap(u,v);
	if(dep[u]-dep[v]==1){
		f.update(dfn[u],lim[u],x);
		g.update(dfn[u],lim[u],x);
		h.update(dfn[u],lim[u],x);
	}else if(dep[u]==dep[v]){
		int w=Fa[u],l=W[w][Len[w]>>1];
		if(Rank[u]>Rank[v]) swap(u,v);
		ring[w].add(Rank[v],x);
		f.update(dfn[w]+1,lim[w],x);
		if(Rank[u]<<1<Len[w]){
			h.update(dfn[w]+1,lim[u],x);
			g.update(dfn[v],dfn[l]-1,x);
			h.update(dfn[l],lim[w],x);
		}else{
			h.update(dfn[w]+1,dfn[l]-1,x);
			g.update(dfn[l],lim[u],x);
			h.update(dfn[v],lim[w],x);
		}
	}else{
		int w=Fa[u],l=W[w][Len[w]>>1];
		bool flag=Rank[u]==1;
		f.update(dfn[w]+1,lim[w],x);
		(flag?g:h).update(dfn[w]+1,dfn[l]-1,x);
		(flag?h:g).update(dfn[l],lim[w],x);
		ring[w].add(flag?1:Len[w],x);
	}
}
inline bool cmp(int i,int j){return dfn[i]<dfn[j];}
void solve_circle(int u,long long &res){
	#define DIS(u,v,f) ((f).sum(dfn[v])-(f).sum(dfn[u]))
	for(int i=Head[u];i;i=Next[i]){
		int v=to[i];
		FOR(k,0,2) sum[u][k]+=sum[v][k];
		if(sum[v][0] && sum[v][1]) res+=DIS(u,v,f);
		else if(sum[v][0]) res+=DIS(u,v,g);
		else if(sum[v][1]) res+=DIS(u,v,h);
	}
}
void solve_square(int u,long long &res){
	static int S[M],p[M];
	int n=vsz[u];
	FOR(i,0,n+1) S[i]=0;
	for(int i=Head[u],m=0;i;i=Next[i]){
		int v=to[i],r=Rank[v];
		FOR(k,0,2) sum[u][k]+=sum[v][k];
		if(sum[v][0] && sum[v][1]) ++S[0];
		else if(sum[v][0]) (r<<1>Len[u])?(++S[n-m]):(++S[0],--S[n-m]);
		else if(sum[v][1]) (r<<1<Len[u])?(++S[n-m]):(++S[0],--S[n-m]);
		p[++m]=r;
	}
	reverse(p+1,p+n+1);
	p[n+1]=Len[u];
	for(auto i:Q[u]){
		int x=px[i],y=py[i];
		if(Rank[x]>Rank[y]) swap(x,y);
		int l=lower_bound(p,p+n,Rank[x])-p;
		int r=lower_bound(p,p+n,Rank[y])-p;
		if(type[i]!=(Rank[y]-Rank[x])<<1<Len[u]) ++S[l],--S[r];
		else ++S[0],++S[r],--S[l];
	}
	Q[u].clear();
	FOR(i,0,n+1){
		S[i+1]+=S[i];
		if(S[i]) res+=ring[u].sum(p[i+1])-ring[u].sum(p[i]);
	}
}
long long query(int k){
	static int A[M<<1],stk[M];
	int vn=0,top=0,tot=0,m;
	FOR(i,0,k){
		int u=X[i],v=Y[i],w=LCA(u,v);
		A[vn++]=u,A[vn++]=v;
		if(w>n){
			px[i]=UP(u,dep[w]+(w>n));
			py[i]=UP(v,dep[w]+(w>n));
			A[vn++]=px[i],A[vn++]=py[i];
			Q[w].push_back(i);
		}else px[i]=py[i]=w;
		++sum[u][type[i]],--sum[px[i]][type[i]];
		++sum[v][type[i]],--sum[py[i]][type[i]];
	}
	sort(A,A+vn,cmp);
	vn=unique(A,A+vn)-A;
	FOR(i,1,vn) A[vn++]=LCA(A[i-1],A[i]);
	sort(A,A+vn,cmp);
	vn=unique(A,A+vn)-A;

	stk[top++]=A[0];
	FOR(i,1,m=vn){
		#define Link(u,v) (Next[++tot]=Head[u],Head[u]=tot,to[tot]=v)
		int u=A[i],v,w;
		while(dfn[u]>lim[stk[top-1]]) --top;
		v=stk[top-1];
		if(v>n && dep[u]>dep[v]+1){
			w=UP(u,dep[v]+1);
			++vsz[w],++vsz[v];
			Link(v,w),Link(w,u);
			stk[top++]=A[vn++]=w;
		}else ++vsz[v],Link(v,u);
		stk[top++]=u;
	}
	inplace_merge(A,A+m,A+vn,cmp);
	vn=unique(A,A+vn)-A;

	long long res=0;
	DOR(i,vn,0){
		if(A[i]<=n) solve_circle(A[i],res);
		else solve_square(A[i],res);
	}
	DOR(i,vn,0) Head[A[i]]=vsz[A[i]]=sum[A[i]][0]=sum[A[i]][1]=0;
	return res;
}

int main(){
	scanf("%d %d %d",&n,&m,&q);
	FOR(i,1,m+1){
		int u,v,c;
		scanf("%d %d %d",&u,&v,&c);
		E[u].push_back(v);
		E[v].push_back(u);
		U[i]=u,V[i]=v,C[i]=c;
	}
	bcc_cnt=n;
	Tarjan(1,-1);
	dfn_cnt=0;
	Get_son(1);
	Get_top(1,1);
	f.New(bcc_cnt+5);
	g.New(bcc_cnt+5);
	h.New(bcc_cnt+5);
	FOR(i,1,m+1) update(U[i],V[i],C[i]);
	while(q--){
		int k,w,x;
		scanf("%d",&k);
		FOR(i,0,k) scanf("%d %d %d",&X[i],&Y[i],&type[i]);
		printf("%lld
",k?query(k):0);
		scanf("%d %d",&w,&x);
		if(w) update(U[w],V[w],x-C[w]),C[w]=x;
	}
	return 0;
}

#总结 虚树还是比较朴素的技巧。 关键是要想到用虚树解。 造出虚树就只有一个树型DP了。 重点是要找到这个模型。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值