• 【暖*墟】#树形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
        }
    }

     

     

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

     

  • 相关阅读:
    JAVA基础--线程
    Flutter: SliverAppBar 应用程序栏与滚动视图集成,以便它可以根据滚动偏移量在高度上变化
    Flutter: MediaQuery
    Dart: puppeteer库
    Flutter: 监听App显示,隐藏
    Dart: 解析html字符串
    Dart: 编码和解码各种存档和压缩格式
    Dart: 执行shell命令
    运行Chrome浏览器如何添加Options
    Flutter 删除AppBar的返回icon
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10498493.html
Copyright © 2020-2023  润新知