虚树+树形dp

虚树实际上是一颗浓缩子树;使用虚树的题目大部分具有查询的点有限,同时虚树构建的信息符合

规则;

做虚树的题目:步骤为先想出原树的暴力求解做法,然后构建虚树同时向其中添加有用信息(比如边权);

虚树的构建过程:

虚树的构建大致有两种,但是两种方式都与dfs序有关;首先解释为什么与dfs序有关

我们可以知道,虚树的构建是求各个关键点两两的lca的过程。如果是两两求lca,暴力构建虚树的话,复杂度会达到k*k*logn;dfs序的出现可以将复杂度降为klogn;dfs序排序可以知道的是,当两个关键点是在同一个链上,那么祖先为其中一个点,并且祖先为深度较小的那个点。当两个关键点不在同一条链上,那么他们的祖先一定是这两个关键点的子树内关键点的lca;

我们做那么一个事情:

如果两两关键点x,y之间的lca为其中一个,那么可以知道他们之间一定没有关键点(因为他们是相邻的两个点,如果中间存在关键点z的话,dfs[x]<dfs[z]<dfs[y],与现有条件冲突),那我们是否可以直接连边呢?我们考虑一下,x,y之间可能出现非关键点吗,是可以的,只存在一种情况:

y后面存在一个点t,使得lca(y,t)在x,y之间,那我们可以知道x->lca(y,t),但是有没有可能存在一个点t1,使得lca(t1,t),使得lca(t1,t)在x,lca(y,t)之间呢,也是存在的。似乎这种解法到了尽头。不妨我们再挖掘一下性质,假设现在按照dfs序排序有a1,a2,a3,a4,a5这5个点;假设我们处理到a4这个点,求出lca(a3,a4),如果lca(a3,a4)在前面没有出现过,那么lca(a3,a4)一定是虚树上面的点,加入集合中(我们也可以一股脑全部加进去,最后去重即可);那么对于lca(a1,a4),lca(a2,a4)呢,这几个点不需要处理吗,难道这两个点一定出现过吗?

下面给出证明:

对于dfs小于a3的点,即ai(1<=i<3)一定在a3的左边或者是祖先;

如果是祖先,那么lca(ai,a4)一定为lca(a3,a4)或者ai;如果不是祖先,那么lca(ai,a4)一定为lca(a3,a4)或者lca(ai,a3);所以一定是出现过呢

一.线性构建

当我们求出虚树的所有点后,将其按照dfs序进行排序,按照dfs序来构建树;

对于x,y,连接lca(x,y)->y,为什么这个就是正确的呢?

如果 x是 y 的祖先,那么直接连边。因为 DFS 序保证x和 y的 DFS 序是相邻的,所以x到y的路径上面没有虚树上的点。

如果 x不是y的祖先,那么就把lca(x,y)当作y的的祖先,因为 DFS 序保证x和 y的 DFS 序是相邻的,同时x,y分别在lca(x,y)的两颗子树上,如果lca(x,y)->y的路径上存在虚树的点,那么这个点才是x,这一点和x,y分别在lca(x,y)的两颗子树上相互矛盾,于是lca(x,y)->y上一定不存在虚树的点。

根据上面的构建,我们将虚树构建出来,所有的虚树的点都存在且结构为一颗树

代码来自虚树 - OI Wiki

int dfn[maxn];
int h[maxn], m, a[maxn], len;  // 存储关键点

bool cmp(int x, int y) {
  return dfn[x] < dfn[y];  // 按照 dfs 序排序
}

void build_virtual_tree() {
  sort(h + 1, h + m + 1, cmp);  // 把关键点按照 dfs 序排序
  for (int i = 1; i < m; ++i) {
    a[++len] = h[i];
    a[++len] = lca(h[i], h[i + 1]);  // 插入 lca
  }
  a[++len] = h[m];
  sort(a + 1, a + len + 1, cmp);  // 把所有虚树上的点按照 dfs 序排序
  len = unique(a + 1, a + len + 1) - a - 1;  // 去重
  for (int i = 1, lc; i < len; ++i) {
    lc = lca(a[i], a[i + 1]);
    conn(lc, a[i + 1]);  // 连边,如有边权 就是 distance(lc,a[i+1])
  }
}

二.单调栈构建

1.对k个关键点进行dfs序排序

2.单调栈维护一个深度由小到大的点

3.考虑加入a[i]

1.假设lca=LCA(sta[top],x),那么如果lca=s[top],直接入栈

如果lca!=sta[top],那么sta[top]和x是lca两颗不同子树内的节点,那么这样的话,低于lca深度的点都需要连边,因为x后面的点和x前面的关键点的公共祖先一定在lca处或者比lca深度更低的点了;

我们一直出栈,直到dep[lca] >dep[sta[top-1]];

如果这时候lca=sta[top],说明lca已经在栈中,直接把a[i]入栈

如果lca!=sta[top],那么不在栈中,将lca与sta[top]连边,同时将a[i]入栈

单调栈的本质是在维护虚树的一条链,链结束的特征是lca的深度,因为我们是dfs序;

 

代码:

bool cmp(int a, int b){
    return dfn[a]<dfn[b];
}
void build(int k){
    sort(a+1,a+k+1,cmp);
    idx=0;
    sta[top=1]=1;
    if(a[1]!=1)sta[++top]=a[1];
    dp[1]=0;
    dp[a[1]]=0;
    for(int i=2; i<=k; i++){
        int l=lca(sta[top],a[i]);
        dp[l]=0;
        dp[a[i]]=0;
        while(top>1 && dep[sta[top-1]]>=dep[l]){
            lca(sta[top-1],sta[top]);
            add(sta[top-1],sta[top],dis);
            top--;
        }
        if(l!=sta[top]){
            lca(sta[top],l);
            add(l,sta[top],dis);
            sta[top]=l;
        }
        sta[++top]=a[i];
    }
    while(top){
        lca(sta[top-1],sta[top]);
        add(sta[top-1],sta[top],dis);
        top--;
    }
}

题目:[SDOI2011] 消耗战 - 洛谷

思路:构建虚树,同时通过倍增预处理,使得我们在lca时求出两个点之间的最小代价;

最后就是一个简单的dp,设dp[u]为以u为根的最小代价;

对于出边v:

如果v是关键点dp[u]+=w[u->v];

如果v不是关键点dp[u]+=min(dp[v],w[u->v]);

具体代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=3*1e5+10,M=5*1e5+10,INF=1e9+11;
int n,m,k,a[N];

int h[N],ne[M<<1],e[M<<1],idx;
LL w[M<<1];
LL dp[N];
int sta[N<<1],top;

int dep[N],fa[N][20],dist[N][20],dis,dfn[N],cnt,siz[N];
void add(int a, int b,int c){
    e[++idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx;
}
LL ans=0;
void dfs(int u, int f){
    dfn[u]=++cnt;
    dep[u]=dep[f]+1;
    fa[u][0]=f;
    for(int i=1; i<=19; i++){
        fa[u][i]=fa[fa[u][i-1]][i-1];
        dist[u][i]=min(dist[u][i-1],dist[fa[u][i-1]][i-1]);
    }
    for(int i=h[u]; i; i=ne[i]){
        int v=e[i];
        if(v==f)continue;
        dist[v][0]=w[i];
        dfs(v,u);
    }
}

int lca(int a, int b){
    dis=INF;
    if(dep[a]<dep[b])swap(a,b);
    for(int i=19; ~i; i--){
        if(dep[fa[a][i]]>=dep[b]){
            dis=min(dis,dist[a][i]);
            a=fa[a][i];
        }
    }
    if(a==b)return b;
    
    for(int i=19; ~i; i--){
         if(fa[a][i]!=fa[b][i]){
            dis=min(dis,dist[a][i]);
            dis=min(dis,dist[b][i]);
            a=fa[a][i];b=fa[b][i];
          
        }
    }
    dis=min(dis,dist[a][0]);
    return fa[a][0];
}
bool cmp(int a, int b){
    return dfn[a]<dfn[b];
}
void build(int k){
    sort(a+1,a+k+1,cmp);
    idx=0;
    sta[top=1]=1;
    if(a[1]!=1)sta[++top]=a[1];
    dp[1]=0;
    dp[a[1]]=0;
    for(int i=2; i<=k; i++){
        int l=lca(sta[top],a[i]);
        dp[l]=0;
        dp[a[i]]=0;
        while(top>1 && dep[sta[top-1]]>=dep[l]){
            lca(sta[top-1],sta[top]);
            add(sta[top-1],sta[top],dis);
            top--;
        }
        if(l!=sta[top]){
            lca(sta[top],l);
            add(l,sta[top],dis);
            sta[top]=l;
        }
        sta[++top]=a[i];
    }
    while(top){
        lca(sta[top-1],sta[top]);
        add(sta[top-1],sta[top],dis);
        top--;
    }
}

void solve(int x){

    for(int i=h[x]; i; i=ne[i]){
            int v=e[i];
            solve(v);
            if(siz[v]){
               
                dp[x]+=w[i];
            }
            else dp[x]+=min(dp[v],w[i]);
          
    }

    h[x]=0;

}

int main(){
    
   cin>>n;
   for(int j=0; j<20; j++)dist[0][j]=1e9;
    for(int i=1; i<n; i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
        for(int j=0; j<20; j++)dist[i][j]=1e9;
    }
    dfs(1,0);

    memset(h,0,sizeof h);
    cin>>m;
    while(m--){
        int k;
        cin>>k;
        for(int i=1; i<=k; i++){
            cin>>a[i];
            siz[a[i]]=1;
            
        }
   
        build(k);
   
  
        solve(1);
        cout<<dp[1]<<endl;
        for(int i=1; i<=k; i++){
            siz[a[i]]=0;
        }
    }
}

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值