虚树,就是不真实的树。往往出现在一类树形动态规划问题中。
换句话说,虚树就是为了解决一类树形动态规划问题而诞生的。
【例题1】[SDOI2011]消耗战
- 给出一棵树,每条边有边权。有m次询问,每次询问给出k个点。
- 问使得这k个点均不与1号点(根节点)相连的最小代价。
- 数据范围:n<=250000, m>=1, ∑k<=500000。
[暴力dp] 首先考虑m=1,也就是只有一次询问的情况。
我们考虑暴力dp:设f[x]为处理完以x为根的树的最小代价。
转移分为两种情况:1.断开自己与根的联系,代价为从根到该节点的最小值;
2.若该节点不是询问点、不考虑断开,求断开子树内的所有询问点的最小代价;
但是这样的复杂度是O(nm)的,显然无法AC。
然而我们发现∑k是比较小的,可不可以对k下手呢?于是,虚树诞生了。
虚树的主要思想:对于一棵树,仅仅保留有用的点,重新构建一棵树。
- 这里有用的点指的是询问点和它们的lca。
比如这样的一棵树:
对于样例中的三次询问:
3
2 10 6 4 5 7 8 3 3 9 4 6
那么它的虚树分别长这样:
- 注意:图二中,断了5就一定会断了7、8,所以不用考虑7、8。
得到了询问点,考虑如何构造出一棵虚树。
首先要先对整棵树dfs一遍,求出dfs序,每个节点以dfs序为关键字从小到大排序。
同时 维护一个栈,表示 从根到栈顶元素这条链 。
假设当前要加入的节点为p,栈顶元素为x=s[top],lca为他们的最近公共祖先。
因为我们是按照dfs序遍历,因此lca不可能是p。
那么现在会有两种情况: 1. lca是x,直接将p入栈。
2. x,p分别位于lca的两棵子树中,那么此时x这棵子树已经遍历完毕。
上述2中,如果没有遍历完毕,则x的子树中还有一个未加入的点y,
但是dfn[y]<dfn[p],即:应先访问y。所以没有遍历完毕。
当x这棵子树已经遍历完毕时,我们需要对其进行构建。
设栈顶元素为x,栈顶第二个元素为y(到达x的某一条链)。
若dfn[y]>dfn[lca],可以连边y—>x,将x出栈;
若dfn[y]=dfn[lca],即y=lca,连边lca−>x,子树构建完毕;
若dfn[y]<dfn[lca],即lca在y,x之间,连边lca−>x,x出栈,lca入栈,子树构建完毕。
不断重复这个过程,虚树就构建完成了。另外需要维护链上最小值,然后直接在虚树上dp。
- 借某大佬的图解:
代码实现: