[SDOI2011]消耗战 (虚树)


题意:

给一棵n个顶点的树,每条树边有边权。
m次询问,每次询问给出k个点,问使得这k个点均不与1号点(根节点)相连的最小代价

数据范围:
在这里插入图片描述

解法:
虚树用法:
在单次询问只涉及树中少量节点时,可以建立一颗只包含关键节点的树
将无用节点组成的链简化为边或者删掉,形成虚树,最后在虚树上进行dp
关键点为询问点和lca

虚数上dp的复杂度:
显然虚数的叶子节点一定是询问点,
当询问中的k个点都是叶子的时虚树形态最大,2k-1个点,树形dp复杂度为O(2k)
虚树形态最大的时是一个二叉树

建树前预处理:
dfs序,lca,将询问点按dfs序排序

建树大致流程:
用栈维护最右链
先无条件将第一个询问点入栈
接下来一次处理询问点,假设当前点为now,now和top的LCA为lc
有4种情况:
1.lc=top,那么now在top的子树中,那么now直接入栈
2.lc在top和top-1之间,那么将top加入虚树,lc和now入栈
3.lc=top-1,此时和2差不多,top加入虚树,lc不入栈,now入栈
4.d[lc]<d[top-1],即lc在top-1上面,此时不断出栈加入虚树直到变为情况2
当所有询问点都处理完之后将最右链加入虚树即可

建完树之后:
标记询问点,对虚树树形dp,只针对询问点dp就行了
回溯的时候顺便清空边数组和询问点的标记

注意事项:
1.当栈中只有一个元素的时候,访问top-1会越界,因此要用数组模拟栈,
且栈从1开始,这样top-1就是0,令d[0]=0就可以了
这样的话会存在0到stk[top]的边,如果建树用的是单向边,不从点0开始dfs就没有影响,双向边的话判断一下点0就行了
2.清空虚树要在dfs访问完一个节点的时候清空才能保证复杂度
不能在最后清空询问点,因为虚树中不止存在询问点,还存在一些lca
3.虚树的根是最后栈中的第一个节点(如果是有向树的话),或者说最后栈中的第一个节点一定在虚树中.
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxm=5e5+5;
int head[maxm],nt[maxm],to[maxm],w[maxm],cnt;
int head2[maxm],nt2[maxm],to2[maxm],cnt2;
int n;
void add(int x,int y,int z){
    cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
void add2(int x,int y){
    cnt2++;nt2[cnt2]=head2[x];head2[x]=cnt2;to2[cnt2]=y;
}
//
const int maxd=20;
int dep[maxm],id[maxm],idx;
int stk[maxm],top;
int f[maxm][25];
bool mark[maxm];
ll mi[maxm];
ll dp[maxm];
void dfs1(int x,int fa){
    id[x]=++idx;
    for(int i=head[x];i;i=nt[i]){
        int v=to[i];
        if(v==fa)continue;
        mi[v]=min(mi[x],1LL*w[i]);
        dep[v]=dep[x]+1;
        f[v][0]=x;
        dfs1(v,x);
    }
}
void lca_init(){
    for(int j=1;j<=maxd;j++){
        for(int i=1;i<=n;i++){
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }
}
int lca(int a,int b){
    if(dep[a]<dep[b])swap(a,b);
    for(int i=maxd;i>=0;i--){
        if(dep[f[a][i]]>=dep[b])a=f[a][i];
    }
    if(a==b)return a;
    for(int i=maxd;i>=0;i--){
        if(f[a][i]!=f[b][i]){
            a=f[a][i],b=f[b][i];
        }
    }
    return f[a][0];
}
bool cmp(int a,int b){
    return id[a]<id[b];
}
void dfs(int x){
    ll temp=0;
    for(int i=head2[x];i;i=nt2[i]){
        int v=to2[i];
        dfs(v);
        temp+=dp[v];
    }
    if(mark[x])dp[x]=mi[x];//如果是标记点
    else dp[x]=min(temp,mi[x]);
    head2[x]=0;//顺便清空
    mark[x]=0;
}
int c[maxm];
void solve(){
    cnt2=0;//记得清空边cnt
    //
    int k;cin>>k;
    for(int i=1;i<=k;i++){
        cin>>c[i];
        mark[c[i]]=1;//标记询问点
    }
    sort(c+1,c+1+k,cmp);
    top=0;
    stk[++top]=c[1];
    for(int i=2;i<=k;i++){
        int now=c[i],lc=lca(now,stk[top]);
        while(1){
            if(dep[lc]>=dep[stk[top-1]]){//可能为123
                if(lc!=stk[top]){//不满足则为1,否则为23
                    add2(lc,stk[top]);//23都要加这条边
                    if(lc!=stk[top-1]){
                        stk[top]=lc;//top出栈,lc入栈
                    }else{
                        top--;//top出栈,lc不入栈
                    }
                }
                break;
            }else{//4
                add2(stk[top-1],stk[top]);
                top--;
            }
        }
        stk[++top]=now;//最后now都要入栈
    }
    while(top>=2){
        add2(stk[top-1],stk[top]);
        top--;
    }
    dfs(stk[1]);//stk[1]是虚树的根
    cout<<dp[stk[1]]<<endl;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n;
    for(int i=1;i<n;i++){
        int a,b,c;cin>>a>>b>>c;
        add(a,b,c);add(b,a,c);
    }
    mi[1]=1e18;
    dfs1(1,-1);
    lca_init();
    int q;cin>>q;
    while(q--){
        solve();
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值