虚树实际上是一颗浓缩子树;使用虚树的题目大部分具有查询的点有限,同时虚树构建的信息符合
规则;
做虚树的题目:步骤为先想出原树的暴力求解做法,然后构建虚树同时向其中添加有用信息(比如边权);
虚树的构建过程:
虚树的构建大致有两种,但是两种方式都与dfs序有关;首先解释为什么与dfs序有关
我们可以知道,虚树的构建是求各个关键点两两的lca的过程。如果是两两求lca,暴力构建虚树的话,复杂度会达到k*k*logn;dfs序的出现可以将复杂度降为klogn;dfs序排序可以知道的是,当两个关键点是在同一个链上,那么祖先为其中一个点,并且祖先为深度较小的那个点。当两个关键点不在同一条链上,那么他们的祖先一定是这两个关键点的子树内关键点的lca;
我们做那么一个事情:
如果两两关键点x,y之间的lca为其中一个,那么可以知道他们之间一定没有关键点(因为他们是相邻的两个点,如果中间存在关键点z的话,dfs[x]<dfs[z]<dfs[y],与现有条件冲突),那我们是否可以直接连边呢?我们考虑一下,x,y之间可能出现非关键点吗,是可以的,只存在一种情况:
y后面存在一个点t,使得lca(y,t)在x,y之间,那我们可以知道x->lca(y,t),但是有没有可能存在一个点t1,使得lca(t1,t),使得lca(t1,t)在x,lca(y,t)之间呢,也是存在的。似乎这种解法到了尽头。不妨我们再挖掘一下性质,假设现在按照dfs序排序有a1,a2,a3,a4,a5这5个点;假设我们处理到a4这个点,求出lca(a3,a4),如果lca(a3,a4)在前面没有出现过,那么lca(a3,a4)一定是虚树上面的点,加入集合中(我们也可以一股脑全部加进去,最后去重即可);那么对于lca(a1,a4),lca(a2,a4)呢,这几个点不需要处理吗,难道这两个点一定出现过吗?
下面给出证明:
对于dfs小于a3的点,即ai(1<=i<3)一定在a3的左边或者是祖先;
如果是祖先,那么lca(ai,a4)一定为lca(a3,a4)或者ai;如果不是祖先,那么lca(ai,a4)一定为lca(a3,a4)或者lca(ai,a3);所以一定是出现过呢
一.线性构建
当我们求出虚树的所有点后,将其按照dfs序进行排序,按照dfs序来构建树;
对于x,y,连接lca(x,y)->y,为什么这个就是正确的呢?
如果 x是 y 的祖先,那么直接连边。因为 DFS 序保证x和 y的 DFS 序是相邻的,所以x到y的路径上面没有虚树上的点。
如果 x不是y的祖先,那么就把lca(x,y)当作y的的祖先,因为 DFS 序保证x和 y的 DFS 序是相邻的,同时x,y分别在lca(x,y)的两颗子树上,如果lca(x,y)->y的路径上存在虚树的点,那么这个点才是x,这一点和x,y分别在lca(x,y)的两颗子树上相互矛盾,于是lca(x,y)->y上一定不存在虚树的点。
根据上面的构建,我们将虚树构建出来,所有的虚树的点都存在且结构为一颗树
代码来自虚树 - OI Wiki
int dfn[maxn];
int h[maxn], m, a[maxn], len; // 存储关键点
bool cmp(int x, int y) {
return dfn[x] < dfn[y]; // 按照 dfs 序排序
}
void build_virtual_tree() {
sort(h + 1, h + m + 1, cmp); // 把关键点按照 dfs 序排序
for (int i = 1; i < m; ++i) {
a[++len] = h[i];
a[++len] = lca(h[i], h[i + 1]); // 插入 lca
}
a[++len] = h[m];
sort(a + 1, a + len + 1, cmp); // 把所有虚树上的点按照 dfs 序排序
len = unique(a + 1, a + len + 1) - a - 1; // 去重
for (int i = 1, lc; i < len; ++i) {
lc = lca(a[i], a[i + 1]);
conn(lc, a[i + 1]); // 连边,如有边权 就是 distance(lc,a[i+1])
}
}
二.单调栈构建
1.对k个关键点进行dfs序排序
2.单调栈维护一个深度由小到大的点
3.考虑加入a[i]
1.假设lca=LCA(sta[top],x),那么如果lca=s[top],直接入栈
如果lca!=sta[top],那么sta[top]和x是lca两颗不同子树内的节点,那么这样的话,低于lca深度的点都需要连边,因为x后面的点和x前面的关键点的公共祖先一定在lca处或者比lca深度更低的点了;
我们一直出栈,直到dep[lca] >dep[sta[top-1]];
如果这时候lca=sta[top],说明lca已经在栈中,直接把a[i]入栈
如果lca!=sta[top],那么不在栈中,将lca与sta[top]连边,同时将a[i]入栈
单调栈的本质是在维护虚树的一条链,链结束的特征是lca的深度,因为我们是dfs序;
代码:
bool cmp(int a, int b){
return dfn[a]<dfn[b];
}
void build(int k){
sort(a+1,a+k+1,cmp);
idx=0;
sta[top=1]=1;
if(a[1]!=1)sta[++top]=a[1];
dp[1]=0;
dp[a[1]]=0;
for(int i=2; i<=k; i++){
int l=lca(sta[top],a[i]);
dp[l]=0;
dp[a[i]]=0;
while(top>1 && dep[sta[top-1]]>=dep[l]){
lca(sta[top-1],sta[top]);
add(sta[top-1],sta[top],dis);
top--;
}
if(l!=sta[top]){
lca(sta[top],l);
add(l,sta[top],dis);
sta[top]=l;
}
sta[++top]=a[i];
}
while(top){
lca(sta[top-1],sta[top]);
add(sta[top-1],sta[top],dis);
top--;
}
}
思路:构建虚树,同时通过倍增预处理,使得我们在lca时求出两个点之间的最小代价;
最后就是一个简单的dp,设dp[u]为以u为根的最小代价;
对于出边v:
如果v是关键点dp[u]+=w[u->v];
如果v不是关键点dp[u]+=min(dp[v],w[u->v]);
具体代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=3*1e5+10,M=5*1e5+10,INF=1e9+11;
int n,m,k,a[N];
int h[N],ne[M<<1],e[M<<1],idx;
LL w[M<<1];
LL dp[N];
int sta[N<<1],top;
int dep[N],fa[N][20],dist[N][20],dis,dfn[N],cnt,siz[N];
void add(int a, int b,int c){
e[++idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx;
}
LL ans=0;
void dfs(int u, int f){
dfn[u]=++cnt;
dep[u]=dep[f]+1;
fa[u][0]=f;
for(int i=1; i<=19; i++){
fa[u][i]=fa[fa[u][i-1]][i-1];
dist[u][i]=min(dist[u][i-1],dist[fa[u][i-1]][i-1]);
}
for(int i=h[u]; i; i=ne[i]){
int v=e[i];
if(v==f)continue;
dist[v][0]=w[i];
dfs(v,u);
}
}
int lca(int a, int b){
dis=INF;
if(dep[a]<dep[b])swap(a,b);
for(int i=19; ~i; i--){
if(dep[fa[a][i]]>=dep[b]){
dis=min(dis,dist[a][i]);
a=fa[a][i];
}
}
if(a==b)return b;
for(int i=19; ~i; i--){
if(fa[a][i]!=fa[b][i]){
dis=min(dis,dist[a][i]);
dis=min(dis,dist[b][i]);
a=fa[a][i];b=fa[b][i];
}
}
dis=min(dis,dist[a][0]);
return fa[a][0];
}
bool cmp(int a, int b){
return dfn[a]<dfn[b];
}
void build(int k){
sort(a+1,a+k+1,cmp);
idx=0;
sta[top=1]=1;
if(a[1]!=1)sta[++top]=a[1];
dp[1]=0;
dp[a[1]]=0;
for(int i=2; i<=k; i++){
int l=lca(sta[top],a[i]);
dp[l]=0;
dp[a[i]]=0;
while(top>1 && dep[sta[top-1]]>=dep[l]){
lca(sta[top-1],sta[top]);
add(sta[top-1],sta[top],dis);
top--;
}
if(l!=sta[top]){
lca(sta[top],l);
add(l,sta[top],dis);
sta[top]=l;
}
sta[++top]=a[i];
}
while(top){
lca(sta[top-1],sta[top]);
add(sta[top-1],sta[top],dis);
top--;
}
}
void solve(int x){
for(int i=h[x]; i; i=ne[i]){
int v=e[i];
solve(v);
if(siz[v]){
dp[x]+=w[i];
}
else dp[x]+=min(dp[v],w[i]);
}
h[x]=0;
}
int main(){
cin>>n;
for(int j=0; j<20; j++)dist[0][j]=1e9;
for(int i=1; i<n; i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
for(int j=0; j<20; j++)dist[i][j]=1e9;
}
dfs(1,0);
memset(h,0,sizeof h);
cin>>m;
while(m--){
int k;
cin>>k;
for(int i=1; i<=k; i++){
cin>>a[i];
siz[a[i]]=1;
}
build(k);
solve(1);
cout<<dp[1]<<endl;
for(int i=1; i<=k; i++){
siz[a[i]]=0;
}
}
}