虚树总结
Alioth_
2019-02-15 20:09:12
### 虚树
有时在做树形$DP$的时候 会出现多组询问 每一组询问指定$m$个点做修改或者别的东西 **且$n$与$\sum m$同阶**
这样的话普通的树形$DP$需要$\Theta(nm)$的复杂度 肯定是过不了 这时就会用到虚树
注意$n$与$\sum m$同阶 每次只是修改$m$个点 这时候就需要用到$\Theta(\sum m)$的算法——虚树
原来的树形$DP$是在原树上$DP$ 跑一边需要$\Theta(n)$ 这样太慢了 注意到每次修改$m$个点 我们就在虚树上保留$m$个点 称为关键点 和他们的$lca$(为了连成一棵树)其他的点合并 这样每次就形成了一棵总复杂度为$\sum m$的虚树 有效地降低了复杂度
如图 红点是关键点 蓝点是$lca$
![](https://cdn.luogu.com.cn/upload/pic/51875.png)
### 构建
我们先在原树上求出$dfs$序 然后把所有的关键点按照$dfs$序排序
这是我们用一个栈来维护 栈中维护一条链 每次插入时 用新点和栈顶的$lca$的$dfn$和栈中第二个点的$dfn$比较 来判断这条链有没有拐弯
若拐弯了 则说明上一个子树已经遍历完 可以构建 我们就不断弹出节点并连边 直到处理完子树 这个判断要用栈中第二个点和$lca$比较 看他是不是在$lca$下面 如果处理完了 还要判断一下子树中是否还有点 如果有 就连$lca$和它 最后把新的点加入栈即可
若没有拐弯 直接加入栈中返回就行了(等处理完这棵子树再连边)
**关键点不一定是虚树的叶子结点**
就这样
```cpp
void insert(int x)//栈中维护的是一条链!!!!!
{
int lca=LCA(s[top],x);
while(top>1&&dfn[s[top-1]]>=dfn[lca])add_edge(s[top-1],s[top]),top--;//s[top]的子树已经遍历完 可以弹出这条链使他拐弯并连s[top]子树中的边
if(lca!=s[top])add_edge(lca,s[top]),s[top]=lca;//弹完后 若栈顶不是lca 则lca一定在他上面 且他是这个子树中最后一个点 连边 加入lca
s[++top]=x;
}
```
### 一些技巧
$1$.连完边后$s[1]$即为$root$ 这个可以确定虚树的根 如果强制以什么东西为根 直接在加入$a$数组前把他加到栈里就行了
$2$.有些题一条链上的点可以合并 如果新加入的点的$lca$是$s[top]$就不用管直接$return$即可
$3$.注意每次构建虚树后要清空连的虚边 新建的边可以用一个$vector$保存 在最后一次在虚树上的$dfs$中直接$v.clear()$即可
$4$.注意虚树上的点和虚树之外的点要分别$DP$
### 例题
#### $1.P3320$寻宝游戏
这题主要是理解虚树思想 其实不用建出虚树 由于每条边遍历两遍的特殊性质 直接用一个$set$维护一下就行了
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=100000+100;
struct edge{
int to,next,w;
}e[maxn<<1];
int head[maxn],tot,n,m,ans,DIS[maxn],siz[maxn],bz[maxn],fa[maxn],dfn[maxn],id,son[maxn],dep[maxn],topf[maxn];
void add(int x,int y,int z)
{
e[++tot].to=y;
e[tot].w=z;
e[tot].next=head[x];
head[x]=tot;
}
struct node{
int id;
};
bool operator <(const node a,const node b)
{
return dfn[a.id]<dfn[b.id];
}
set<node> s;
void dfs1(int x,int f)
{
siz[x]=1;
fa[x]=f;
for(int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if(y==f)continue;
dep[y]=dep[x]+1;
DIS[y]=DIS[x]+e[i].w;
dfs1(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])son[x]=y;
}
}
void dfs2(int x,int tp)
{
topf[x]=tp;
dfn[x]=++id;
if(!son[x])return ;
dfs2(son[x],tp);
for(int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);
}
}
inline int lca(int x,int y)
{
while(topf[x]!=topf[y])
{
if(dep[topf[x]]>dep[topf[y]])x=fa[topf[x]];
else
y=fa[topf[y]];
}
if(dep[x]<dep[y])swap(x,y);
return y;
}
inline int dis(int x,int y)
{
return DIS[x]+DIS[y]-2*DIS[lca(x,y)];
}
inline set<node>::iterator last(set<node>::iterator Pos)
{
if(Pos==s.begin())Pos=s.end();
Pos--;
return Pos;
}
inline set<node>::iterator next(set<node>::iterator Pos)
{
Pos++;
if(Pos==s.end())Pos=s.begin();
return Pos;
}
std::set<node>::iterator l,r;
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
dfs1(1,0);
dfs2(1,0);
for(int i=1;i<=m;i++)
{
int u;
scanf("%lld",&u);
if(s.find((node){u})!=s.end())
{
if(s.size()!=1)
{
l=r=s.find(node{u});
l=last(l),r=next(r);
ans-=dis(u,(*l).id)+dis(u,(*r).id);
ans+=dis((*l).id,(*r).id);
}
s.erase((node){u});
}
else
{
s.insert((node){u});
if(s.size()!=1)
{
l=r=s.find((node){u});
l=last(l),r=next(r);
ans+=dis(u,(*l).id)+dis(u,(*r).id);
ans-=dis((*l).id,(*r).id);
}
}
printf("%lld\n",ans);
}
}
```
#### $2.P2495$消耗战
这题也有点特殊 每次只用$DP$虚树上的点 同时建虚树时一条链上的点也可以合并 且强制以$1$为根 不够典型 但$DP$好写
```cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=250001;
int n,m;
struct edge{
int next,to,w,from;
}e[maxn<<1];
int head[maxn],num;
inline void add(int x,int y,int z)
{
e[++num].to=y;
e[num].from=x;
e[num].w=z;
e[num].next=head[x];
head[x]=num;
}
vector<int>v[maxn];
inline void add_edge(int x,int y)
{
v[x].push_back(y);
}
int a[maxn],dfn[maxn],top,topf[maxn],siz[maxn],son[maxn],s[maxn],fa[maxn],deep[maxn],id;
ll mn[maxn];
void dfs1(int x,int f)
{
siz[x]=1;
fa[x]=f;
for(int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if(y==f)continue;
deep[y]=deep[x]+1;
mn[y]=min(mn[x],(ll)e[i].w);
dfs1(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])son[x]=y;
}
}
void dfs2(int x,int tp)
{
topf[x]=tp;
dfn[x]=++id;
if(!son[x])return ;
dfs2(son[x],tp);
for(int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);
}
}
inline bool cmp(int x,int y)
{
return dfn[x]<dfn[y];
}
int LCA(int x,int y)
{
while(topf[x]!=topf[y])
{
if(deep[topf[x]]>deep[topf[y]])x=fa[topf[x]];
else
y=fa[topf[y]];
}
if(deep[x]<deep[y])swap(x,y);
return y;
}
//此题特殊 不做模板题
void insert(int x)//栈中维护的是一条链!!!!!
{
if(top==1){s[++top]=x;return ;}
int lca=LCA(s[top],x);
if(lca==s[top])return ;//一条链上的需求点可以用第一个点替代后面的不用加入
while(top>1&&dfn[s[top-1]]>=dfn[lca])add_edge(s[top-1],s[top]),top--;//s[top]的子树已经遍历完 可以弹出这条链使他拐弯并连s[top]子树中的边
if(lca!=s[top])add_edge(lca,s[top]),s[top]=lca;//弹完后 若栈顶不是lca 则lca一定在他上面 且他是这个子树中最后一个点 连边 加入lca
s[++top]=x;
}
ll dp(int x)
{
if(!v[x].size())return mn[x];
ll sum=0;
for(int i=0;i<v[x].size();i++)
{
int y=v[x][i];
sum+=dp(y);
}
v[x].clear();
return min(sum,mn[x]);
}
int main()
{
scanf("%d",&n);
mn[1]=1ll<<60;
for(int i=1;i<n;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
deep[1]=1;
dfs1(1,0);
dfs2(1,0);
scanf("%d",&m);
while(m--)
{
int k;
scanf("%d",&k);
for(int i=1;i<=k;i++)
scanf("%d",&a[i]);
sort(a+1,a+1+k,cmp);
s[top=1]=1;
for(int i=1;i<=k;i++)insert(a[i]);
while(top>0)
add_edge(s[top-1],s[top]),top--;
cout<<dp(1)<<endl;
}
}
```
#### $3.P3233$世界树
这才是真正的模板题 ~~虽然DP实在是太恶心~~ 但所有的特点都体现了 比如虚树内外的点分别$DP$ 插入也比较正常 但是DP太傻逼了 调了一天
```cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn=309010;
const int INF=1000010000;
typedef pair<int,int>p;
struct edge{
int next,to;
}e[maxn<<1];
int head[maxn],rt,num,fa[maxn][30];
inline void add(int x,int y)
{
e[++num].to=y;
e[num].next=head[x];
head[x]=num;
}
vector<int>v[maxn];
inline void add_edge(int x,int y){v[x].push_back(y);}
int a[maxn],n,q,bo[maxn],A[maxn],up[maxn],bel[maxn],ans[maxn],dis[maxn],dfn[maxn],top,topf[maxn],siz[maxn],son[maxn],s[maxn],deep[maxn],id;
void dfs1(int x,int f)
{
dfn[x]=++id;
siz[x]=1;
fa[x][0]=f;
for(int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if(y==f)continue;
deep[y]=deep[x]+1;
dfs1(y,x);
siz[x]+=siz[y];
}
}
//计算儿子对父亲的贡献
void dfs3(int x)//dis[x]表示在以x为根的子树内,离x最近的管辖节点到x的距离
{
if(bo[x])
bel[x]=x,dis[x]=0;//叶子结点管辖自己 一定是管辖点
else bel[x]=0,dis[x]=INF;//初值
for(int i=0;i<v[x].size();i++)
{
int y=v[x][i];
dfs3(y);
if(dis[y]+deep[y]-deep[x]<dis[x]||(dis[y]+deep[y]-deep[x]==dis[x]&&bel[x]>bel[y]&&bel[y]))
{
dis[x]=dis[y]+deep[y]-deep[x];
bel[x]=bel[y];
}
}
}
//计算父亲对儿子的贡献 因为有可能被兄弟管辖 所以dfs3先到lca dfs4再下到这个节点
void dfs4(int u,int D,int x)
{
if(D<dis[u]||(D==dis[u]&&bel[u]>x))dis[u]=D,bel[u]=x;
else D=dis[u],x=bel[u];//一直记录父亲的 否则记录父亲的bel
for(int i=0;i<v[u].size();i++)
{
dfs4(v[u][i],D+deep[v[u][i]]-deep[u],x);
}
}
//处理虚树子树中的节点归谁
void dfs5(int x)
{
ans[bel[x]]+=siz[x];
for(int i=0;i<v[x].size();i++)
{
int y=v[x][i];//x的虚树儿子
for(int j=18;j>=0;j--)
if(fa[y][j]&&deep[fa[y][j]]>deep[x])y=fa[y][j];//ans[bel[x]]要减去所有x子树中有虚树节点的子树siz 只保留没有虚树节点的子树
ans[bel[x]]-=siz[up[v[x][i]]=y];
dfs5(v[x][i]);
}
}
//现在只剩下虚树边上的被我们忽略的节点没有确定归属。
void dfs6(int x)
{
for(int i=0;i<v[x].size();i++)
{
int y=v[x][i];
if(bel[x]==bel[y]){ans[bel[x]]+=siz[up[v[x][i]]]-siz[y];dfs6(v[x][i]);continue;}
int H=deep[bel[y]]+deep[x]-dis[x];
H=H&1?H+1>>1:(bel[y]<bel[x]?H>>1:(H>>1)+1);
for(int j=18;j>=0;j--)
if(fa[y][j]&&deep[fa[y][j]]>=H)y=fa[y][j];
ans[bel[v[x][i]]]+=siz[y]-siz[v[x][i]];
ans[bel[x]]+=siz[up[v[x][i]]]-siz[y];
dfs6(v[x][i]);
}
}
void dfs7(int x)
{
up[x]=bel[x]=dis[x]=0;
for(int i=0;i<v[x].size();i++)dfs7(v[x][i]);
v[x].clear();
}
inline bool cmp(int x,int y)
{
return dfn[x]<dfn[y];
}
inline int LCA(int x,int y)
{
if(deep[x]>deep[y])swap(x,y);//x比y浅
for(int i=18;i>=0;i--)
if(deep[fa[y][i]]>=deep[x])y=fa[y][i];
if(x==y)return x;
for(int i=18;i>=0;i--)
if(fa[y][i]!=fa[x][i])y=fa[y][i],x=fa[x][i];
return fa[x][0];
}
void insert(int x)//栈中维护的是一条链!!!!!
{
int lca=LCA(s[top],x);
while(top>1&&dfn[s[top-1]]>=dfn[lca])add_edge(s[top-1],s[top]),top--;//s[top]的子树已经遍历完 可以弹出这条链使他拐弯并连s[top]子树中的边
if(lca!=s[top])add_edge(lca,s[top]),s[top]=lca;//弹完后 若栈顶不是lca 则lca一定在他上面 且他是这个子树中最后一个点 连边 加入lca
s[++top]=x;
}
int main()
{
// freopen("10.in","r",stdin);
// freopen("new 1.ans","w",stdout);
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
deep[1]=1;
dfs1(1,0);
for(int i=1;i<=18;i++)
for(int j=1;j<=n;j++)
fa[j][i]=fa[fa[j][i-1]][i-1];
scanf("%d",&q);
while(q--)
{
int m;
scanf("%d",&m);
for(int i=1;i<=m;i++)
scanf("%d",&a[i]),A[i]=a[i],bo[a[i]]=1;
sort(a+1,a+1+m,cmp);
s[top=1]=a[1];
for(int i=2;i<=m;i++)
insert(a[i]);
while(top>1)
add_edge(s[top-1],s[top]),top--;
rt=s[1];
dfs3(rt);
dfs4(rt,dis[rt],bel[rt]);
dfs5(rt);
dfs6(rt);
ans[bel[rt]]+=siz[1]-siz[rt];
for(int i=1;i<=m;i++)
cout<<ans[A[i]]<<" ";
cout<<endl;
dfs7(rt);
for(int i=1;i<=m;i++)
bo[a[i]]=ans[a[i]]=0;
}
}
```
### 没了