过不去那道题?
事实:它数据范围很大
而且每一次询问都得O(n)暴力
出题人最坏了!
介绍……虚树!
介绍
先看这么一道题
题目大意:每一次给k个特殊点,每次可以删除多条边,代价为边权和。求让这些特殊点与根节点不联通的最小代价
这道题的暴力dp非常好想:令 mi[i] 表示从该点到根节点的路径中最小的边权,则转移方程为:
\[dp[x]=min(\sum_{son[x]}^{}mi[son[x]],mi[x]) \]时间复杂度 O(nm) ,爆炸
我们不难发现,对于答案有影响的只有特殊点以及他们之间的LCA,由于 \(∑k i≤5\times 10^5\) ,理论上是可以过的。这时候我们只取这些点构建一个树,构建的这棵树就被叫做虚树
构建方式
构建方式比较难想,我就不写思路直接写方式了
我们将所有特殊点按照搜索序排序(假设为dfn),并考虑对于虚树内的节点维护一个dfn递增的单调栈,且保证栈内每个点dfn均小于还未处理的特殊点。
我们先将点1压入节点。假设此时处理到了特殊点x,从栈顶取出元素top,并与x求lca,假设为l。由于l为x与top的祖先(即dfn[l]<=dfn[top]),且我们保证dfn[l]<dfn[x],所以一共有两种情况:
1.dfn[l]<dfn[top] 由于dfn[x]<dfn[top],说明x、所有剩下需要处理的点也top并不在l的同一棵子树上,也就是说top所在的那颗子树我们已经构建完毕了,我们尝试去构建它
由于对于栈内每一个节点y,由于我们在构建y时,他的祖先所有dfn更小的子树都被构建完成了,所以对于stk[i],stk[i-1]必然为其虚树上的父亲。换言之,栈内的节点依次连边,构成了虚树上的最右链
(以上可能比较绕,配合图片可能好理解一点,可惜我蒟蒻不会画图QAQ,可以拿样例自己画着试一试)
既然知道了这点我们就很好做了:对于栈顶的前两个元素stk[i]和stk[i-1],如果dfn[stk[i-1]]>=dfn[l],就说明stk[i-1]和stk[i]都在l的子树上,直接把他们连边,再让栈顶出栈就好了,一直到dfn[stk[i-1]]<dfn[l]
这是我们又碰到了一个问题:这时候栈顶top的dfn还是小于等于l的,也就是说top有可能在l的子树上。这时候该怎么办呢?分两种情况讨论:
如果l==top,那什么操作都不用做
如果dfn[l]<dfn[top],说明l在top与top-1的中间。那么我们将l与top连边,然后将top出栈,l入栈即可。
以上我们讨论了dfn[l]<dfn[top],如果l=top就更简单了,说明虚树内top就是x的父亲,x就在最右链上,直接把x入栈即可
代码:
sort(p+1,p+m+1,cmp);
stk[top=1]=1;//将1入栈
for(int i=1;i<=m;i++){
int x=p[i];
if(x==1) continue;
if(top==1){//为了保证根节点为1,需要保证全程top>=1
stk[++top]=x;
continue;
}
int l=lca(stk[top],x);
/*if(l==top)
continue;
对例题的特殊处理,如果x的祖先本来就在虚树上,x就没有加入的必要了
但建普通虚树时就不必加上这句
*/
while(top>1&&dfn[stk[top-1]]>=dfn[l]){
//由于dfn[stk[top-1]]<dfn[top]<=dfn[l],所以我们不需要额外判断l==top
sadd(stk[top-1],stk[top]);//建边
top--;
}
if(dfn[l]<dfn[stk[top]]&&top>1){
sadd(l,stk[top]);
stk[top]=l;//等价于栈顶出栈l入栈
}
stk[++top]=x;
}
for(;top>0;top--){
//别忘了把最右链上剩下的点加进虚树
sadd(stk[top-1],stk[top]);
}
最后在虚树上跑一遍算法就好了
小结
难的不是建出虚树,而是在构建出虚树后怎么用它。比如这道P3233 [HNOI2014]世界树。即使构建除了世界树,仍然需要分类讨论虚树上的子节点、虚树间的链、虚树上的节点之间比较复杂(恶心)的关系。让我刷到这道题的时候不断怀疑自己直接建虚树是不是假的
不过绝大多数情况下,看到多次询问,涉及节点总数量和单次询问复杂度均在一个可以接受的范围内就大胆构建虚树吧,说不定就蒙对了呢(
标签:构建,top,stk,dfn,二年级,能看懂,节点,虚树 From: https://www.cnblogs.com/WhangZjian/p/16732572.html