虚树
当一棵树很大但是有用的点很少的时候,虚树可以有效地减少点数,从而降低复杂度。
配套题单
全部例题代码
一道例题
[SDOI2011]消耗战
题目简述:给你一个\(n\)个点的树,边有边权,然后有\(m\)个询问,每次给你\(k\)个关键点,问你切断一些边使得1号点不与任何一个关键点连通的最小代价,切断一条边的代价为这条边的权值。
数据范围:\(n \le 2.5 \times 10^5\),\(m \le 5 \times 10^5\),\(\sum k \le 5 \times 10^5\)。
我们很容易想到一种dp的方法。
设\(f_x\)表示使\(x\)不与其子树中任意一个关键点连通的最小代价。
然后我们枚举\(x\)的儿子\(t\):
- 若\(t\)为一个关键点,则\(f_x=f_x+w(x,t)\)。
- 若\(t\)不为一个关键点,则\(f_x=f_x+min\{w(x,t),f_t\}\)。
复杂度\(O(nm)\)。
然后我们发现树上有很多个点都不是关键点,是没有必要对它进行dp的,于是我们把一颗大树浓缩成一颗小树,也就是虚树。
虚树的构建
我们考虑哪些点是应当出现在虚树上的,关键点显然需要,同时,为了满足虚树中的父子关系不发生改变,关键点与关键点的LCA也需要保留。
我们不能\(O(k^2)\)枚举LCA,需要优化。
我们有一个单调栈做法:
- 我们首先将1号点加入虚树,并压入单调栈。
- 单调栈维护一条链的信息。
- 按照 DFS 序从小到大添加关键节点。
- 假如当前的节点与栈顶节点的LCA就是栈顶节点的话,则说明它们是在一条链上的。所以直接把当前节点入栈就行了。
- 假如当前节点与栈顶节点的LCA不是栈顶节点的话,就需要弹出栈顶元素,直到当前节点与栈顶节点的LCA为栈顶节点为LCA,同时记得把他们在虚树中向他们的父亲连边(栈中的上一个点),如果弹完以后发现LCA不在栈中,需要压入栈。最后将当前节点压入栈中。
举个例子
如下图,我们要构建4,6,7三个节点的虚树:
首先按照dfs序排序,得到序列\([4,6,7]\)。
然后将一号点加入栈中。
如图:
我们用红色的点代表在栈内的点,青色的点代表从栈中弹出的点。
然后取出序列中的第一个点4,与栈顶的点1,求\(LCA(1,4)\),等于栈顶元素,将4入栈。
取出序列中的第二个点6,与栈顶的点4,求\(LCA(4,6)\),不等于栈顶元素。
将6与栈中第二个元素1连边,弹出6。
\(LCA(1,4)\)等于此时的栈顶元素,且LCA在栈中,将6压入栈后结束,此时栈内元素\([1,6]\)。
取出序列中的第三个点7,与栈顶的点6,求\(LCA(6,7)=3\),不等于栈顶元素。
发现栈中第二个元素1的dfs序小于LCA的dfs序,说明LCA没有入栈,将7与3连边,弹出7,然后向栈中加入LCA,即3号点。
然后将7号点压入栈。
此时序列已经遍历完毕,发现栈中还有点,将它们从下往上依次连边,然后弹出栈。
最后建立出来的虚树长这样。
注意,每一次构建虚树不要把数组全部清空,那样复杂度会炸掉,只要把要用到的位置清空就好了。
虚树建立的代码:
void build()
{
sort(h+1,h+k+1,cmp);
top=1;st[1]=1;G[1].clear();
for(int i=1;i<=k;i++)
if(h[i]!=1)
{
int Lca=LCA(h[i],st[top]);
if(Lca!=st[top])
{
while(id[Lca]id[st[top-1]])
G[Lca].clear(),G[Lca].push_back(st[top]),st[top]=Lca;
else
G[Lca].push_back(st[top]),top--;
}
G[h[i]].clear();st[++top]=h[i];
}
for(int i=1;i
建立完虚树后,虚树上的两点的边权应当为原树中两点中间的所有边的权值最小值。
然后再按照上面的dp思路去做就好了。
例题&练习
[SDOI2011]消耗战
如上。
[HNOI2014]世界树
题意简述:给定一颗\(n\)个节点的树,每一次给你\(k\)个关键点,每一个节点会对距离它最近的编号最小的关键造成贡献,问每个关键点的贡献。
首先我们在原树上跑dfs求出dfs序,深度和大小,然后建立虚树,然后考虑如何计算贡献。
数据范围:\(N\leq 300000\), \(q\leq 300000\), \(\sum^q_{i=1}m_i \leq 300000\)
贡献分为三个部分:
-
虚树上的点的贡献。
对于这部分的点我们求出每个虚树上的节点距离最近的关键点的编号,以及到它的距离,直接统计答案即可。 -
虚树上相邻点之间的点及其子树的贡献。
我们可以找到一个分界点,满足它上面的部分属于距离父亲最近的关键点,它及其它下面的部分属于距离儿子最近的关键点。 -
虚树上的点在原树中的某个没有任何一个虚树上的点的子树。
我们遍历虚树,对于每一个虚树上的点的大小,减去它的所有在虚树上的儿子往上倍增到的距离这个点最近的点的大小,贡献到距离这个点最近的关键点上。
[ARC086C] Smuggling Marbles
题意简述:
给你一棵\(n\)个节点的树,以\(0\)为根,每一个点上面都可能有一个石子,每一次你要做出如下操作:
- 取走\(0\)号点上的石子。
- 把所有石子从儿子移到其父亲上。
- 任意时刻,如果一个点上出现两个及更多石子,这些石子会被移除(不会算作答案)。
最后问你这\(2^{n+1}\)种石子的分布情况,你可以取走的石子数的总和为多少。
数据范围:\(1 \le n \le 2 \times 10^5\)。
首先发现对于每一层的石子都是独立的,因此对于每一层的所有点建立虚树,分别来做。
然后再虚树上做dp,设\(sum0_x\)表示\(x\)号点上面没有石子的情况数,\(sum1_x\)表示\(x\)号点上面有一个石子的情况数。
- \(sum1_x=sum0_x=1\)(\(x\)在虚树中没有儿子)。
- \(sum1_x=\sum_{i=son} sum1_i \prod_{j=son,j \neq i} sum0_i,sum0_x=\prod_{i=son}(sum0_i+sum1_i) -sum1_x\)(\(x\)在虚树中有儿子)。
然后对答案的贡献就是\(sum1_0 \times 2^{n+1-S}\)。
Treeland and Viruses
题意简述:有一棵有 \(n\) 个节点的树,\(q\) 次询问(询问互相独立),每次给定 \(k_i\) 个颜色,每个颜色有一个起始点 \(v_j\)?和移动速度 \(s_j\),每一个颜色在每一次操作中会使它周围没有被染色的连通块上与它的距离不超过 \(s_j\) 的点全部染为这一个颜色,每一轮中,颜色从 \(1\) 到 \(k_i\) 依次开始操作,一直到所有点全部被染色为止,再询问 \(m_i\)个关键点的颜色。
数据范围:\(1 \le n,q,\sum k_i,\sum m_i \le 2 \times 10^5\),\(1 \le s_j \le 10^6\)
傻逼黑题,建完虚树后就等价于树上多源最短路径,dp即可。
[HNOI/AHOI2018]毒瘤
此题码农题
题意简述:求一个图的独立集方案数。
\(n\le 10^5,n-1 \le m \le n+10\),保证图联通
我们先来考虑一下树的情况怎么做。
老套路,设\(f_{x,0}\)表示\(x\)不选时的方案数,\(f_{x,1}\)表示\(x\)选时的方案数。
设\(t\)为\(x\)的儿子,则有:
- \(f_{x,0}=\prod_t f_{t,0}+f_{t,1}\)
- \(f_{x,1}=\prod_t f_{t,0}\)
考虑图的情况,图的独立集的方案数是一个NPC问题,但我们发现这个图上的非树边最多只有11条,因此我们可以枚举每一条非树边的一个端点选还是不选,由此决定另外一个端点可不可以选,然后重新进行dp。
时间复杂度:\(O(n 2^{m-n+1})\)。
考虑优化,我们发现每一次dp转移发生了改变的只有少部分点,大部分点的转移不会发生改变。
因此我们可以把所有非树边连接的点作为关键点,然后对于这些关键点建立虚树。
我们每一次枚举后都只在虚树上dp,复杂度就会降低许多,但是这样转移的方程式就会改变。
- \(f_{x,0}=\prod_t k_1 f_{t,0}+k_2 f_{t,1}\)
- \(f_{x,1}=\prod_t k_3 f_{t,0}+k_4 f_{t,1}\)
其中\(k1,k2,k3,k4\)都是常数。
我们考虑如何来求出这些常数。
我们在每一个非虚树上的点的联通快进行dp即可,具体做法就是进行dfs,遇到非虚树上的点就进行转移,遇到虚树上的点就不进行转移,就可以了。
[NOI2021] 庆典
此题码农题
题意简述:
给出一张无向图满足若\(x \Longrightarrow z\),\(y \Longrightarrow z\)那么有\(x \Longrightarrow y\)或\(y \Longrightarrow x\)。
\(q\)次询问给出起点和终点和\(k\)条临时的边,求可能经过点的数量。
\(1 \le n,q \le 3 \times 10^5,0 \le k \le 2\)。
我们先对原图缩点转DAG,然后我们发现如果\(x\)连向\(y\),\(y\)连向\(z\),\(x\)连向\(z\)时,\(x\)连向\(y\)这一条并没有用,我们把这些边删掉以后原图转为一颗叶向树。
然后我们取出所有新加的边的两个端点以及起点和终点,建立一颗虚树以及虚树的反图,在虚树及其反图上加上新加的边。
接着,我们就可以暴力从\(s\)点走虚树,得到一个点集;从\(t\)点走虚树的反图,得到另一个点集。然后两个集合的交集大小就是答案。
Bear and Chemistry
此题码农题
题意简述:给定一张\(n\)个点\(m\)条边的初始无向图。
\(q\)次询问,每次询问给定一个点集\(V\)和边集\(E\)。
你需要判断,将\(E\)中的边加入初始无向图之后,\(V\)中任意两个点\(x,y\)是否都能在每条边至多经过一次的情况下从\(x\)到\(y\)再回到\(x\)。
\(n,m,q,\sum|V|,\sum |E| \le 3 \times 10^5\),强制在线。
这题比较简单,无非就是问你这些点是否在同一个点双里。
先把原图缩边双,变成一颗树,对于每一个询问,把给定的点和连接的边的两端加入虚森林中,然后再将新加的边加入虚森林中,再跑一次边双联通分量就好了。
注意点:
- 原图不联通。
- 本题有重边,处理边双的时候要注意一点。
- 建虚森林的时候要注意对每一个联通块建虚树。
- 这题有强制在线,不要忘记处理了。
- 本题时无向图,虚森林连边要连两次。
Surprise me!
题意简述:给定一棵\(n\)个点的树,点有点权\(a_i\),点权为\(1 ... n\)的一个排列,现在要你求出:
\[\frac 1 {n(n-1)} \sum_{i=1}^n \sum_{j \neq i} \varphi (a_i \times a_j) \times dist(i,j) \]数据范围:\(1 \le n \le 2 \times 10^5\)
我们发现\(\varphi(a \times b)=\frac{\varphi(a) \times \varphi(b) \times gcd(a,b)}{\varphi(gcd(a,b))}\),然后就是老套路了。
枚举\(gcd=d\),我们设\(Ans_d=\sum_{i=1}^n \sum_{j \neq i} \varphi (a_i) \times \varphi(a_j) \times (dep[i]+dep[j]-2 dep[Lca]) [gcd(a_i,a_j)==d]\)
设\(g_d=\sum_{i=1}^n \sum_{j \neq i,gcd(a_i,a_j)|d} \varphi (a_i) \times \varphi(a_j) \times (dep[i]+dep[j]-2 dep[Lca])\),这个很好求,只需要对于每一个\(d\)建一棵以点权为\(d\)的倍数的点为关键点的虚树,然后在\(Lca\)处统计子树的答案就行了。
倒着求出\(g_d\),然后容斥就可以求出\(Ans_d=g_d-\sum_{i \times d \le n,i \ge 2} Ans_{i \times d}\)。
然后\(ans=\frac 1 {n(n-1)} \sum_{i=1}^n \frac i {\varphi(i)} Ans_i\)。
练习题(都比较水)
- [HEOI2014]大工程
- Envy
- Kingdom and its Cities
- [GZOI2017]共享单车
- Tree
- [SDOI2018]战略游戏
- [SDOI2015]寻宝游戏
- 树上的毒瘤{此题码农题}