虚树学习笔记
1. 简介
虚树,顾名思义,就是不真实的树,常用于动态规划,所以可以说,虚树就是为了解决一类动态规划问题而诞生的
当一次询问中仅涉及一颗树中的少量节点时,在整棵树上dp时间复杂度显然难以接受
所以可以建立一颗只包含关键节点的树,将非关键的链简化或省略,在新树上进行dp
一颗虚树包含所有询问点及它们的lca,所以所有询问点为叶子节点,所以,这棵树至多有2k-1个节点,大大降低了时间复杂度
2.例题
从一道例题入手
2.1. 题面
[SDOI2011] 消耗战
题目描述
在一场战争中,战场由 \(n\) 个岛屿和 \(n-1\) 个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为 \(1\) 的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他 \(k\) 个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到 \(1\) 号岛屿上)。不过侦查部门还发现了这台机器只能够使用 \(m\) 次,所以我们只需要把每次任务完成即可。
输入格式
第一行一个整数 \(n\),表示岛屿数量。
接下来 \(n-1\) 行,每行三个整数 \(u,v,w\) ,表示 \(u\) 号岛屿和 \(v\) 号岛屿由一条代价为 \(w\) 的桥梁直接相连。
第 \(n+1\) 行,一个整数 \(m\) ,代表敌方机器能使用的次数。
接下来 \(m\) 行,第 \(i\) 行一个整数 \(k_i\) ,代表第 \(i\) 次后,有 \(k_i\) 个岛屿资源丰富。接下来 \(k_i\) 个整数 \(h_1,h_2,..., h_{k_i}\) ,表示资源丰富岛屿的编号。
输出格式
输出共 \(m\) 行,表示每次任务的最小代价。
样例 #1
样例输入 #1
10
1 5 13
1 9 6
2 1 19
2 4 8
2 3 91
5 6 8
7 5 4
7 8 31
10 7 9
3
2 10 6
4 5 7 8 3
3 9 4 6
样例输出 #1
12
32
22
提示
数据规模与约定
- 对于 \(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\) 。
2.2. 暴力
当只看一次查询时,记\(f_i\)为处理完i的最小代价
考虑转移,分两种情况:
- 断开与父亲的连接
- 断开子树内所有资源点与该点的连接,前提是该点没有资源
显然,时间复杂度为\(O(nm)\),会T
考虑优化,因为\(\sum k\)较小,所以可以从k入手
优化先放一下,我们先学虚树再做题
3. 虚树
3.1. 建树
首先对整棵树dfs,求出dfs序,再按dfs序将询问点排序
同时维护一个栈,表示根到栈顶元素这一条链,即最右链
假设当前加入的节点为p,栈顶元素为x,lca为它们的最近公共祖先
因为是按dfs序遍历,所以\(lca\neq p\)
分为两种情况
- lca=x,则p直接入栈
- x,p分别位于两棵不同的子树,必然x所在子树已经遍历完了(如果没有,则必然存在一个一个点y还为入栈,但因为\(dfn_y<dfn_p\),则应该先访问y,与假设矛盾),我们要对其进行构建
对于情况2又分为如下情况:
记\(x=s_{top},y=s_{top-1}\)
- lca在x和y之间
显然此时最右链的末端由y->x,变为y->lca->x,所以,要先把lca-x这条边加入,然后x出栈,lca和p入栈
- lca=y
该情况与情况1基本相同,但lca不用入栈了
- 此时lca已经不在y的子树中,甚至不在\(s_{top-2},s_{top-3},\dots\)的子树中
以上图为例,最右链由\(s_{top-3}->s_{top-2}->y->x\)变为\(s_{top-3}->lca->p\),所以要循环将末枝剪下,知道不在是情况3
3.2. 回归例题
以询问2为例
建立虚树后是:
在建树的同时,可以记录1到p的路径上的最小边权minv,如果p是询问点,直接切断p及其子树上询问点的最小代价\(f_p=minv_p\),否则最小代价为\(f_p=\min(minv_p,\sum_{v\in son(p)} f_v)\)
但虽然p是询问点,其子树仍需遍历,因为需要清空
代码:
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,head[250005],edgenum,q,k,h1[250005];
struct edge{
int to,nxt,val;
}e[500005],e1[500005];
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
void add_edge1(int u,int v)
{
e1[++edgenum].nxt=h1[u];
e1[edgenum].to=v;
h1[u]=edgenum;
}
int f[250005][20],dep[250005],dfn[250005],idx;
ll minv[250005];
void dfs(int u,int fa)
{
dfn[u]=++idx;
f[u][0]=fa;
dep[u]=dep[fa]+1;
for(int i=1;i<20;i++)
{
f[u][i]=f[f[u][i-1]][i-1];
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
minv[v]=min(minv[u],1ll*e[i].val);
dfs(v,u);
}
}
int lca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=19;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
x=f[x][i];
}
if(x==y) return x;
}
for(int i=19;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int a[250005],st[250005],top;
bool vis[250005];
bool cmp(int x,int y)
{
return dfn[x]<dfn[y];
}
ll dp(int u,int fa)
{
ll sum=0,res;
for(int i=h1[u];i;i=e1[i].nxt)
{
int v=e1[i].to;
if(v==fa) continue;
sum+=dp(v,u);
}
if(vis[u])
{
res=minv[u];
}
else
{
res=min(minv[u],sum);
}
vis[u]=0,h1[u]=0;
return res;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
add_edge(v,u,w);
minv[i]=1e15;
}
minv[n]=1e15;
dfs(1,0);
scanf("%d",&q);
while(q--)
{
scanf("%d",&k);
for(int i=1;i<=k;i++)
{
scanf("%d",&a[i]);
vis[a[i]]=1;
}
sort(a+1,a+k+1,cmp);
top=1;
st[top]=a[1];
edgenum=0;
for(int i=2;i<=k;i++)
{
int now=a[i];
int lc=lca(now,st[top]);
while(1)
{
if(dep[lc]>=dep[st[top-1]])
{
if(lc!=st[top])
{
add_edge1(lc,st[top]);
add_edge1(st[top],lc);
if(lc!=st[top-1])
{
st[top]=lc;
}
else top--;
}
break;
}
else
{
add_edge1(st[top-1],st[top]);
add_edge1(st[top],st[top-1]);
top--;
}
}
st[++top]=now;
}
while(--top)
{
add_edge1(st[top],st[top+1]);
add_edge1(st[top+1],st[top]);
}
printf("%lld\n",dp(st[1],0));
}
return 0;
}