算法入门:虚树
_MengShang_ · · 算法·理论
参考资料:https://oi-wiki.org//graph/virtual-tree/
目录
-
虚树
1-1. 引入
1-2. 朴素做法
1-3. 优化做法
1-3-1. 观察
1-3-2. 虚树
1-3-2-1. 二次排序+LCA连边
1-3-2-2. 单调栈
1-3-3. 虚树上的DP
1-4. 代码 -
练习
2-1. [HNOI2014] 世界树
2-1-1. 思路
2-1-2. 代码
2-2. [HEOI2014] 大工程
2-2-1. 思路
2-2-2. 代码
2-3. Kingdom and its Cities
2-3-1. 思路
2-3-2. 代码
2-4. [SDOI2018] 战略游戏
2-5. [WC2018] 通道
正文
1. 虚树
1-1. 引入
题目: [SDOI2011] 消耗战
题目描述:
在一场战争中,战场由
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到
输入:
第一行一个整数
接下来
第
接下来
输出:
输出共
数据规模与约定:
- 对于
10\% 的数据,n\leq 10, m\leq 5 。 - 对于
20\% 的数据,n\leq 100, m\leq 100, 1\leq k_i\leq 10 。 - 对于
40\% 的数据,n\leq 1000, 1\leq k_i\leq 15 。 - 对于
100\% 的数据,2\leq n \leq 2.5\times 10^5, 1\leq m\leq 5\times 10^5, \sum k_i \leq 5\times 10^5, 1\leq k_i< n, h_i\neq 1, 1\leq u,v\leq n, 1\leq w\leq 10^5 。
1-2. 朴素做法
显然,若总点数较少,则可以直接对每一次询问跑
称某次询问中被选中的点为 关键点 ,
记
记
则最终答案即为
对于遍历到的点
- 若
y 为 关键点,则dp(x)=dp(x)+w(x,y) - 若
y 不为 关键点,则dp(x)=dp(x)+\min \{ dp(y),w(x, y) \}
如此,便能
然而,本题的数据范围为
1-3. 优化做法
1-3-1. 观察
打表发现,对于每次
- 根节点
1 - 关键点
- 在关键点中任意取
2 个点,它们的 最近公共祖先
接着便要用到虚树了。
1-3-2. 虚树
当 查询多,关键点少,在原树上 有用点稀疏 时,可以将整棵树的有用信息压缩成一棵点数较少的树,即由 有用点 构成的 虚树,再进行操作,而非直接在原树上进行。
然而,我们不可能枚举所有的取关键点情况,
下面给出两种建树方法。
1-3-2-1. 二次排序+LCA连边
之所以枚举的复杂度为
我们建立点集
接着将关键点 按
由于
把
为什么可以直接连
这是因为,对于点
- 若
x 是y 的祖先节点,则\text{lca}(x,y) 即为x 。由于 不存在 点z \in \complement_A{\{x,y\}} 的\text{DFS} 序在x 和y 之间,故x 到y 的路径上 没有 其它A 中的点,可以直接连。 - 若
x 不是y 的祖先节点,对于\text{lca}(x,y) 和y ,同样 不存在z \in \complement_A{\{x,y\}} 在\text{lca}(x,y) 到y 的路径之间,可以直接连。
这里我们可以用反证法:若存在z \in \complement_A{\{x,y\}} 在\text{lca}(x,y) 到y 的路径之间,则dfn(x)<dfn(z)<dfn(y) ,不满足A 按\text{DFS} 排序后的性质,故命题得证。
这样,我们便用二次排序
时间复杂度为
伪代码:
int dfn[N]; // DFS序
int h[N], k, a[N << 1], tot;
// h为输入的关键点序,a即上文中的A,空间注意要开两倍,因为未去重前有2k个点
inline bool cmp(int x, int y) {
return dfn[x] < dfn[y]; // 按DFS序升序排序
}
void buildVirtualTree() {
sort(h + 1, h + k + 1, cmp); // 将关键点按DFS序排序
a[tot = 1] = 1; // 加入根节点1
for (int i = 1; i < k; ++i) {
a[++tot] = h[i]; // 加入关键点
a[++tot] = lca(h[i], h[i + 1]); // 加入LCA
} a[++tot] = h[k]; // 加入最后一个关键点
sort(a + 1, a + tot + 1, cmp); // 二次排序
tot = unique(a + 1, a + tot + 1) - a - 1; // 去重
for (int i = 1; i < tot; ++i)
connect(lca(a[i], a[i + 1]), a[i + 1]); // 连边
}
1-3-2-2. 单调栈
除了二次排序
明确单调栈用途:维护目前虚树中最右端的一条链。
具体操作:
- 把关键点集按
\text{DFS} 序排序。 - 将 根节点
1 压入栈中。 - 顺序遍历关键点集,对于当前遍历到的点
x ,记当前栈顶元素为y :- 如果
x=y ,跳过。 - 如果
\text{lca}(x,y)=y ,说明x 和y 在一条链上,直接将x 压入栈。 - 如果
\text{lca}(x,y)\neq y ,说明x 和y 不在同一条链上,则我们需要将栈顶与次栈顶连边,并弹栈顶,直到y_{new} 与x 在同一条链上,即\text{lca}(y_{new},x)=y_{new} ,再把x 压入栈中。
- 如果
- 将栈中剩余的那条链弹出并记录。
伪代码:
int dfn[N]; // DFS序
int sta[N], top; // 栈
int h[N], k; // 输入的关键点集
inline bool cmp(int x, int y) {
return dfn[x] < dfn[y]; // 按DFS序升序排序
}
void buildVirtualTree() {
sort(h + 1, h + k + 1, cmp);
sta[top = 1] = 1;
for (int i = 1; i <= k; ++i) {
int x = h[i], y = sta[top];
if (x == y) continue; // 如果x为栈顶元素,直接跳过
int lc = lca(x, y);
if (lc != y) { // 如果x与y不在同一条链上
while (dfn[lc] < dfn[sta[top - 1]]) {
connect(sta[top - 1], sta[top]); // 连边
--top;
} // 将sta中的与x所在链没有重合部分的边连接并弹出
if (lc != sta[top - 1]) { // 如果lc不是次栈顶,即lc在栈顶到次栈顶的边上
connect(lc, sta[top]); // 连一条lc->sta[top]的边
sta[top] = lc; // 将原栈顶弹出,压入lc
}
else { // 如果lc就是次栈顶
connect(lc, sta[top]); // 连边
--top; // 弹出栈顶
}
}
sta[++top] = x; // 将x压入栈
}
for (int i = 1; i < top; ++i) {
connect(sta[i], sta[i + 1]); // 处理最后一条链
}
}
1-3-3. 虚树上的DP
通过上面内容,我们已经建出了虚树,接下来就可以思考原
记
记
在虚树上遍历。
对于遍历到的点
- 若
y 不为关键点,temp=temp+\min \{ dp(y),w(y) \} - 若
y 为关键点,temp=temp+w(y)
其中,
最终答案为
1-4. 代码(以二次排序+LCA连边为例)
code
2. 练习
2-1. [HNOI2014] 世界树
2-1-1. 思路:
按照模板建完虚树后,我们发现这题的
下面给出
- 我们先求出虚树上每个点属于那个关键点,在进行遍历。
- 遍历时,如果两个点的所属相同,直接统计答案,否则倍增找分割点,再统计答案。
具体见下:
在每个点建立二元组
定义二元组
第一遍遍历,用子节点
- 若该点为关键点,则其上二元组为
(x,0) - 若该点不为关键点,则其上二元组为
(z,dis_{z,x}) ,满足z \in \{key_y\} ,且(z,dis_{z,x}) 取到最小
第二遍遍历,用当前节点
- 若
(key_x,dis_{y,key_x}) < (key_y,dis_y) ,则令(key_y,dis_y) \leftarrow (key_x,dis_{y,key_x})
第三遍遍历,统计最终答案。对于当前遍历到达的点
- 记
tmp = siz_x - 对于每一个子节点
y :- 记其所在那条链上,
x 的子节点为yy -
tmp=tmp-siz_{yy} - 若
key_y = key_x ,则令cnt_{key_x}=cnt_{key_{x}}+siz_{yy}-siz_y - 若
key_y \neq key_x ,则倍增跳到点z ,使key_z=key_y 且key_{fa[z]}=key_x ,
此时在令:cnt_{key_x} &\leftarrow cnt_{key_x}+siz_{yy}-siz_z \\ cnt_{key_y} &\leftarrow cnt_{key_x}+siz_z-siz_y \end{aligned} - 继续向下遍历
- 记其所在那条链上,
- 将剩余的
tmp 加在cnt_{key_x} 上
2-1-2. 代码:
code
2-2. [HEOI2014] 大工程
2-2-1. 思路
本题每次询问有三个输出,我们分开来思考。
先建一颗虚树,其上每条边的边权为两端点在原树上的距离。
对于第一个输出,询问 新通道的代价和 ,我们只需将虚树上每条边乘以它的贡献次数即可。
注意到,对于虚树上边
对于第二个输出,询问 新通道中代价最小的是多少 。我们在遍历时维护每个点到其子树中关键点的最短路长度和次短路长度(这两个关键点不应属于该点的同一个子节点),最后相加,再在所有点的最小值
对于第三个输出,询问 新通道中代价最大的是多少 ,我们参照输出二,只是将维护内容改为最大和次大,最终再所有点的 最大值
注意开
2-2-2. 代码
code
2-3. Kingdom and its Cities
2-3-1. 思路
首先我们发现,若存在父子节点都是关键点,则敌方目标不能达成,答案为
然后我们再来考虑敌方至少要占几个非关键点。
先建虚树,再进行
对于遍历到的点
- 若
x 是关键点,将它与需要断的y 分别断开。 - 若
x 不是关键点,若有大于一个的y 需要断开则将其本身断开,若只有一个则留到其父节点再判断,若都不是则直接跳过。
统计所有断开点即为答案。
2-3-2. 代码
code