【暖*墟】#树形DP# 虚树的学习与练习

虚树,就是不真实的树。往往出现在一类树形动态规划问题中。

换句话说,虚树就是为了解决一类树形动态规划问题而诞生的。

 

【例题1】[SDOI2011]消耗战  

 

  • 给出一棵树,每条边有边权。有m次询问,每次询问给出k个点。
  • 问使得这k个点均不与1号点(根节点)相连的最小代价。
  • 数据范围:n<=250000, m>=1, ∑k<=500000。

 

[暴力dp] 首先考虑m=1,也就是只有一次询问的情况。

我们考虑暴力dp:设f[x]为处理完以x为根的树的最小代价。

转移分为两种情况:1.断开自己与根的联系,代价为从根到该节点的最小值;

2.若该节点不是询问点、不考虑断开,求断开子树内的所有询问点的最小代价;

 

但是这样的复杂度是O(nm)的,显然无法AC。

 

然而我们发现∑k是比较小的,可不可以对k下手呢?于是,虚树诞生了。

 

虚树的主要思想:对于一棵树,仅仅保留有用的点,重新构建一棵树。

  • 这里有用的点指的是询问点和它们的lca。

 

比如这样的一棵树:

 

对于样例中的三次询问:

3
2 10 6
4 5 7 8 3
3 9 4 6

 

那么它的虚树分别长这样:

          

  • 注意:图二中,断了5就一定会断了7、8,所以不用考虑7、8。

 

得到了询问点,考虑如何构造出一棵虚树。

首先要先对整棵树dfs一遍,求出dfs序,每个节点以dfs序为关键字从小到大排序。

 

 

同时 维护一个栈,表示 从根到栈顶元素这条链

 

假设当前要加入的节点为p,栈顶元素为x=s[top],lca为他们的最近公共祖先。

 

因为我们是按照dfs序遍历,因此lca不可能是p。

 

那么现在会有两种情况: 1. lca是x,直接将p入栈。

2. x,p分别位于lca的两棵子树中,那么此时x这棵子树已经遍历完毕。

 

上述2中,如果没有遍历完毕,则x的子树中还有一个未加入的点y,

但是dfn[y]<dfn[p],即:应先访问y。所以没有遍历完毕。

 

当x这棵子树已经遍历完毕时,我们需要对其进行构建。

 

设栈顶元素为x,栈顶第二个元素为y(到达x的某一条链)。

若dfn[y]>dfn[lca],可以连边y—>x,将x出栈;

若dfn[y]=dfn[lca],即y=lca,连边lca−>x,子树构建完毕;

若dfn[y]<dfn[lca],即lca在y,x之间,连边lca−>x,x出栈,lca入栈,子树构建完毕。

 

不断重复这个过程,虚树就构建完成了。另外需要维护链上最小值,然后直接在虚树上dp。


 

 

 

void insert(int x){ //建立虚树
    
    if(top==1){ sta[++top]=x; return; }
    int lca=LCA(x,sta[top]); if(lca==sta[top]) return;

    //1.若dfn[y]>dfn[lca],可以连边y—>x,将x出栈;
    while(top>1&&dfn[sta[top-1]]>=dfn[lca]) add_edge(sta[top-1],sta[top]),top--;
    
    if(lca!=sta[top]) add_edge(lca,sta[top]),sta[top]=lca; sta[++top]=x;
}

 

  • 借某大佬的图解:

 

【p2495】消耗战  代码实现:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

//【p2495】消耗战 // 树形dp + 虚树

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int N=250019; int n,m;

struct Edge{ int u,v,w,nextt; }e[N*2];

int head[N],tot=0;

inline void add(int x,int y,int z)
 { e[++tot]=(Edge){x,y,z,head[x]}; head[x]=tot; }

vector<int> v[N]; void add_edge(int x,int y){ v[x].push_back(y); }

int top_[N],siz[N],son[N],dep[N],fa[N];

int dfn[N],ID=0,a[N],sta[N],top; ll minn[N]; 

//-------------树链剖分部分-----------------//

void dfs1(int u,int _fa){
    siz[u]=1,fa[u]=_fa;
    for(int i=head[u];i!=-1;i=e[i].nextt){
        if(e[i].v==_fa) continue; dep[e[i].v]=dep[u]+1;
        minn[e[i].v]=min(minn[u],(ll)e[i].w); //到根节点的链上的最小边
        dfs1(e[i].v,u),siz[u]+=siz[e[i].v];
        if(siz[e[i].v]>siz[son[u]]) son[u]=e[i].v; }
}

void dfs2(int x,int top_fa){
    top_[x]=top_fa,dfn[x]=++ID; if(!son[x]) return;
    dfs2(son[x],top_fa); //先走重儿子
    for(int i=head[x];i!=-1;i=e[i].nextt) 
        if(!top_[e[i].v]) dfs2(e[i].v,e[i].v);
}

int LCA(int x,int y){
    while(top_[x]!=top_[y]){
        if(dep[top_[x]]<dep[top_[y]]) swap(x,y);
        x=fa[top_[x]];
    } if(dep[x]<dep[y]) swap(x,y); return y;
}

//------------虚树树形DP部分---------------//

void insert(int x){ //建立虚树
    
    if(top==1){ sta[++top]=x; return; }
    int lca=LCA(x,sta[top]); if(lca==sta[top]) return;

    //1.若dfn[y]>dfn[lca],可以连边y—>x,将x出栈;
    while(top>1&&dfn[sta[top-1]]>=dfn[lca]) add_edge(sta[top-1],sta[top]),top--;
    
    if(lca!=sta[top]) add_edge(lca,sta[top]),sta[top]=lca; sta[++top]=x;
}

ll dp(int x){ //在虚树上直接dp
    if(v[x].size()==0) return minn[x]; 
    ll sum=0; for(int i=0;i<v[x].size();i++) sum+=dp(v[x][i]);
    v[x].clear(); return min(sum,(ll)minn[x]);
}

//-------------主程序部分-----------------//

bool cmp(const int &a,const int &b){ return dfn[a]<dfn[b]; }

int main(){
    
    memset(head,-1,sizeof(head)); minn[1]=1ll<<60; reads(n);
    
    for(int i=1,x,y,z;i<n;i++)
        reads(x),reads(y),reads(z),add(x,y,z),add(y,x,z);
    
    dep[1]=1,dfs1(1,0),dfs2(1,1); reads(m);

    while(m--){ int k; reads(k);
        for(int i=1;i<=k;i++) reads(a[i]);
        sort(a+1,a+k+1,cmp); //将这些节点以dfn序排序
        sta[top=1]=1; for(int i=1;i<=k;i++) insert(a[i]);
        while(top>0) add_edge(sta[top-1],sta[top]),top--; //虚树连边
        cout<<dp(1)<<endl; //在虚树上直接树形dp
    }
}

 

 

                                                ——时间划过风的轨迹,那个少年,还在等你。

 

原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10498493.html