虚树
引子¶
「SDOI2011」消耗战
题目描述¶
在一场战争中,战场由
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到
输入格式¶
第一行一个整数
接下来 n-1 行,每行三个整数
第
接下来
输出格式¶
输出有
数据范围¶
对于
虚树 Virtual Tree¶
对于上面那题,我们不难发现——如果树的点数很少,那么我们可以直接跑 DP。
首先我们称某次询问中被选中的点为—— 「关键点」 。
设
设
则枚举
- 若
v Dp(i)=Dp(i) + \min \{Dp(v),w(i,v)\} - 若
v Dp(i)=Dp(i) + w(i,v)
很好,这样我们得到了一份
听起来很有意思。
我们不难发现——其实很多点是没有用的。以下图为例:
如果我们选取的关键点是:
图中只有两个红色的点是 关键点 ,而别的点全都是「非关键点」。
对于这题来说,我们只需要保证红色的点无法到达
通过肉眼观察可以得出结论——
观察题目给出的条件,红色点(关键点)的总数是与
因此我们需要 浓缩信息,把一整颗大树浓缩成一颗小树 。
由此我们引出了 「虚树」 这个概念。
我们先直观地来看看虚树的样子。
下图中,红色结点是我们选择的关键点。红色和黑色结点都是虚树中的点。黑色的边是虚树中的边。
因为任意两个关键点的 LCA 也是需要保存重要信息的,所以我们需要保存它们的 LCA,因此虚树中不一定只有关键点。
不难发现虚树中祖先后代的关系并不会改变。(就是不会出现原本
但我们不可能
因为可能多个节点的 LCA 可能是同一个,所以我们不能多次将它加入虚树。
非常直观的一个方法是:
- 将关键点按 DFS 序排序;
for
一遍,任意两个相邻的关键点求一下 LCA,并且哈希表判重;- 然后根据原树中的祖先后代关系建树。
朴素算法的复杂度较高。因此我们提出一种单调栈做法。
在提出方案之前,我们先确认一个事实——在虚树里,只要保证祖先后代的关系没有改变,就可以随意添加节点。
也就是,如果我们乐意,我们可以把原树中所有的点都加入虚树中,也不会导致 WA(虽然会导致 TLE)。
因此,我们为了方便,可以首先将
好,开始讲怎么用单调栈来建立一棵虚树吧。
首先我们要明确一个目的——我们要用单调栈来维护一条虚树上的链。
也就是一个栈里相邻的两个节点在虚树上也是相邻的,而且栈是从底部到栈首单调递增的(指的是栈中节点 DFS 序单调递增),说白了就是某个节点的父亲就是栈中它下面的那个节点。
首先我们在栈中添加节点
然后接下来按照 DFS 序从小到大添加关键节点。
假如当前的节点与栈顶节点的 LCA 就是栈顶节点的话,则说明它们是在一条链上的。所以直接把当前节点入栈就行了。
假如当前节点与栈顶节点的 LCA 不是栈顶节点的话:
这时,当前单调栈维护的链是:
而我们需要把链变成:
那么我们就把蓝色结点弹栈即可,在弹栈前别忘了向它在虚树中的父亲连边。
假如弹出以后发现栈首不是 LCA 的话要让 LCA 入栈。
再把当前节点入栈就行了。
下面给出一个具体的例子。假设我们要对下面这棵树的 4,6 和 7 号结点建立虚树:
那么步骤是这样的:
- 将 3 个关键点
6,4,7 [4,6,7] - 将
1
我们用黄色的点代表在栈内的点,绿色的点代表从栈中弹出的点。
- 取序列中第一个作为当前节点,也就是
4 1 1 4 LCA LCA(1,4)=1 - 发现
LCA(1,4)= 4 4,1
- 取序列第二个作为当前节点,为
6 4 6 4 LCA LCA(6,4)=1 - 发现
LCA(6,4)\neq - 判断阶段:发现栈顶节点
4 LCA(6,4) 1 LCA LCA 1->4 LCA 4
- 结束了判断阶段,将
6 6,1
- 取序列第三个作为当前节点,为
7 6 7 6 LCA LCA(7,6)=3 - 发现
LCA(7,6)\neq - 判断阶段:发现栈顶节点
6 LCA(7,6) 1 LCA LCA 3->6 LCA 6 LCA(6,7) - 结束了判断阶段,将
7 1,3,7
- 发现序列里的 3 个节点已经全部加入过栈了,退出循环。
- 此时栈中还有 3 个节点:
1, 3,7 1->3 3->7 - 虚树就建完啦!
我们接下来将那些没入过栈的点(非青绿色的点)删掉,对应的虚树长这个样子:
其中有很多细节,比如我是用邻接表存图的方式存虚树的,所以需要清空邻接表。但是直接清空整个邻接表是很慢的,所以我们在 有一个从未入栈的元素入栈的时候清空该元素对应的邻接表 即可。
建立虚树的 C++ 代码大概长这样:
代码实现
sort(h + 1, h + 1 + k, cmp);
sta[top = 1] = 1, g.sz = 0, g.head[1] = -1;
// 1号节点入栈,清空1号节点对应的邻接表,设置邻接表边数为1
for (int i = 1, l; i <= k; i += 1)
if (h[i] != 1) { // 如果1号节点是关键节点就不要重复添加
l = lca(h[i], sta[top]); // 计算当前节点与栈顶节点的LCA
if (l != sta[top]) {
// 如果LCA和栈顶元素不同,则说明当前节点不再当前栈所存的链上
while (id[l] < id[sta[top - 1]])
// 当次大节点的Dfs序大于LCA的Dfs序
g.push(sta[top - 1], sta[top]), top--;
// 把与当前节点所在的链不重合的链连接掉并且弹出
if (id[l] > id[sta[top - 1]])
// 如果LCA不等于次大节点(这里的大于其实和不等于没有区别)
g.head[l] = -1, g.push(l, sta[top]), sta[top] = l;
// 说明LCA是第一次入栈,清空其邻接表,连边后弹出栈顶元素,并将LCA入栈
else
g.push(l, sta[top--]);
// 说明LCA就是次大节点,直接弹出栈顶元素
}
g.head[h[i]] = -1, sta[++top] = h[i];
// 当前节点必然是第一次入栈,清空邻接表并入栈
}
for (int i = 1; i < top; i += 1)
g.push(sta[i], sta[i + 1]); // 剩余的最后一条链连接一下
于是我们就学会了虚树的建立了!
对于消耗战这题,直接在虚树上跑最开始讲的那个 DP 就行了,我们等于利用了虚树排除了那些没用的非关键节点!仍然考虑
- 若
v Dp(i)=Dp(i) + \min \{Dp(v),w(i,v)\} - 若
v Dp(i)=Dp(i) + w(i,v)
于是这题很简单就过了。
推荐习题¶
buildLast update and/or translate time of this article,Check the history
editFound smelly bugs? Translation outdated? Wanna contribute with us? Edit this Page on Github
peopleContributor of this article HeRaNO, Ir1d, konnyakuxzy, ksyx, Xeonacid, konnyakuxzy, greyqz, sshwy
translateTranslator of this article Visit the original article!
copyrightThe article is available under CC BY-SA 4.0 & SATA ; additional terms may apply.