虚树学习笔记
1. 简介
虚树,顾名思义,就是不真实的树,常用于动态规划,所以可以说,虚树就是为了解决一类动态规划问题而诞生的
当一次询问中仅涉及一颗树中的少量节点时,在整棵树上dp时间复杂度显然难以接受
所以可以建立一颗只包含关键节点的树,将非关键的链简化或省略,在新树上进行dp
一颗虚树包含所有询问点及它们的lca,所以所有询问点为叶子节点,所以,这棵树至多有2k-1个节点,大大降低了时间复杂度
2.例题
从一道例题入手
2.1. 题面
[SDOI2011] 消耗战
题目描述
在一场战争中,战场由
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到
输入格式
第一行一个整数
接下来
第
接下来
输出格式
输出共
样例 #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
提示
数据规模与约定
- 对于
的数据, 。 - 对于
的数据, 。 - 对于
的数据, 。 - 对于
的数据, 。
2.2. 暴力
当只看一次查询时,记
考虑转移,分两种情况:
- 断开与父亲的连接
- 断开子树内所有资源点与该点的连接,前提是该点没有资源
显然,时间复杂度为
考虑优化,因为
优化先放一下,我们先学虚树再做题
3. 虚树
3.1. 建树
首先对整棵树dfs,求出dfs序,再按dfs序将询问点排序
同时维护一个栈,表示根到栈顶元素这一条链,即最右链
假设当前加入的节点为p,栈顶元素为x,lca为它们的最近公共祖先
因为是按dfs序遍历,所以
分为两种情况
- lca=x,则p直接入栈
- x,p分别位于两棵不同的子树,必然x所在子树已经遍历完了(如果没有,则必然存在一个一个点y还为入栈,但因为
,则应该先访问y,与假设矛盾),我们要对其进行构建
对于情况2又分为如下情况:
记
- lca在x和y之间
显然此时最右链的末端由y->x,变为y->lca->x,所以,要先把lca-x这条边加入,然后x出栈,lca和p入栈
- lca=y
该情况与情况1基本相同,但lca不用入栈了
- 此时lca已经不在y的子树中,甚至不在
的子树中
以上图为例,最右链由
3.2. 回归例题
以询问2为例
建立虚树后是:
在建树的同时,可以记录1到p的路径上的最小边权minv,如果p是询问点,直接切断p及其子树上询问点的最小代价
但虽然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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律