虚树是什么?
在 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了。 重点是要找到这个模型。