引入

例题:[BZOJ-2286]消耗战

在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为1的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他k个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。

侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到1号岛屿上)。不过侦查部门还发现了这台机器只能够使用m次,所以我们只需要把每次任务完成即可。

考虑到每次资源重新分布后只需要断开K个岛屿的连通性,所以树上有很多点是没有用的。如果可以从原树中抽出一部分节点,保持原有的父子关系,使得这些点仅仅包含被选中的k个节点和一部分他们的LCA,节点间的边权为他们在原树上的距离,那么问题就能得到简化。

关键问题是如何将对答案有影响的点抽出来并且重构成一棵树。此时引入虚树的概念。顾名思义,虚树是从原树中抽象出来的一棵树。

虚树构造

首先引入单调栈的概念,单调栈维护的是原树中的一条链,栈中每一个节点的父亲(广义)为其相邻的前一个节点。构造的具体步骤如下:

1.将根入栈
2.按照DFS序从小到大对待处理的节点依次判断,若该节点与栈首的LCA就是栈首,那么说明这个节点仍然在维护的链上,直接入栈即可。
3.若LCA不是栈首,那么开始弹栈,直到栈首节点后一个节点的DFS序小于等于LCA的DFS序则停止。若LCA就是栈首后一个节点,则将栈首与LCA连边,弹栈;否则再将LCA入栈。

4.让待处理节点入栈

5.最后所有节点入栈后,对栈内的节点进行连边。虚树到此构造完毕。

模板

注:来自于题目[BZOJ-2286]

#pragma GCC optimize(3)
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#define N 300010
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;

int n,m,i,k,lsum=1,cnt,L,Lsum=1,x,y,c;
int head[N],id[N],f[N][22],a[N],v[N],d[N],p[30],Head[N],b[N],z[N],cost[N][22],df[N];
LL dp[N];

vector <int> son[N];

struct Edge{
    int t,next,d;
}e[N*4],E[N*4];

inline void Swap(int &a,int &b){a^=b^=a^=b;}
inline LL Min(LL a,LL b){return (a<b)?a:b;}
inline bool cmp(int a,int b){return (id[a]<id[b]);}

inline int read(){
    int p=0;    char    c=getchar();
    while (c<48||c>57)  c=getchar();
    while (c>=48&&c<=57)    p=(p<<1)+(p<<3)+c-48,c=getchar();
    return p;
}

inline void Add(int &s,int &t,int &l){
    e[lsum].t=t;    e[lsum].next=head[s];   e[lsum].d=l;    head[s]=lsum++;
    e[lsum].t=s;    e[lsum].next=head[t];   e[lsum].d=l;    head[t]=lsum++;
}

inline void Maketree(int x){
    int i=0,t=0;
    v[x]=1;
    for (i=head[x];i;i=e[i].next){
        t=e[i].t;
        if (v[t])   continue;
        son[x].push_back(t);    f[t][0]=x;
        d[t]=d[x]+1;    cost[t][0]=e[i].d;
        Maketree(t);
    }
}

inline void Dfs(int x){
    int i=0,s=0;
    id[x]=++cnt;
    for (i=1;i<=21;i++){
        f[x][i]=f[f[x][i-1]][i-1];
        cost[x][i]=Min(cost[x][i-1],cost[f[x][i-1]][i-1]);
    }
    if (!son[x].size()) return;
    for (s=son[x].size(),i=0;i<s;i++)   Dfs(son[x][i]);
}

inline void Ready(){
    for (i=1,p[0]=1;i<=21;i++)  p[i]=p[i-1]*2;
    d[1]=1; Maketree(1);    Dfs(1);
}

inline int LCA(int x,int y){
    int l=0,i=0;    L=INF;
    if (d[x]<d[y])  Swap(x,y);
    l=d[x]-d[y];
    for (i=0;i<=21;i++)
        if (l&p[i]) L=Min(L,cost[x][i]),x=f[x][i];
    if (x==y)   return x;
    for (i=21;i>=0;i--)
        if (f[x][i]!=f[y][i])   L=Min(L,cost[x][i]),x=f[x][i],y=f[y][i];
    return f[x][0];
}

inline void ADD(int x,int y){
    LCA(x,y);
    E[Lsum].t=y;    df[y]=L;    E[Lsum].next=Head[x];   Head[x]=Lsum++;
}

inline void DP(int x){
    int i=0,r=0;
    dp[x]=0;
    if (!Head[x])   return;
    for (i=Head[x];i;i=E[i].next){
        r=E[i].t;   DP(r);
        (b[r]!=(n+m))?dp[x]+=Min(dp[r],df[r]):dp[x]+=df[r];
    }
}

inline void Work(){
    int i=0,p=0;
    sort(a+1,a+1+k,cmp);
    Lsum=z[z[0]=1]=1;   Head[1]=0;
    for (i=1;i<=k;i++){
        p=LCA(a[i],z[z[0]]);
        if (p!=z[z[0]]){
            while (id[z[z[0]-1]]>id[p]) ADD(z[z[0]-1],z[z[0]]),z[0]--;
            if (p!=z[z[0]-1])   Head[p]=0,ADD(p,z[z[0]]),z[z[0]]=p;
                else ADD(p,z[z[0]--]);
        }
        Head[a[i]]=0;   z[++z[0]]=a[i];
    }
    for (i=1;i<z[0];i++)    ADD(z[i],z[i+1]);
    DP(1);  printf("%lld\n",dp[1]);
}

int main(){
    n=read();
    for (i=1;i<n;i++){
        x=read();   y=read();   c=read();
        Add(x,y,c);
    }
    Ready();    m=read();
    while (m--){
        k=read();
        for (i=1;i<=k;i++)  a[i]=read(),b[a[i]]=n+m;
        Work();
    }
    return 0;
}

相关例题

[HEOI2014] 大工程

题目描述

国家有一个大工程,要给一个非常大的交通网络里建一些新的通道。

我们这个国家位置非常特殊,可以看成是一个单位边权的树,城市位于顶点上。

在两个国家a,b之间建一条新通道需要的代价为树上a,b的最短路径。

现在国家有很多个计划,每个计划都是这样,我们选中了k个点,然后在它们两两之间 新建

C_{k}^{2}

条新通道。现在对于每个计划,我们想知道:

  1. 这些新通道的代价和
  2. 这些新通道中代价最小的是多少
  3. 这些新通道中代价最大的是多少

解法

从原树中抽出被选中的节点构造虚树,然后统计每个节点对应子树下有多少被选中的节点,通过计算边权的贡献算总距离,然后DP计算最大和最小代价。

代码

#pragma GCC optimize(3)
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#define N 1000010
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;

int n,i,x,y,k,lsum=1,mians,L,q,top,mxans,cnt;
int dp[N][3],f[N][25];
LL Ans;
LL size[N];
int a[N],b[N],v[N],head[N],id[N],d[N],p[25],z[N];
vector <int> s[N],S[N];

inline void Swap(int &a,int &b){a^=b^=a^=b;}
inline int Min(int a,int b){return (a<b)?a:b;}
inline int Max(int a,int b){return (a>b)?a:b;}
inline bool cmp(const int a,const int b){return (id[a]<id[b]);}

struct Edge{
    int t,next,d;
}e[N*4];

inline int read(){
    int p=0;    char    c=getchar();
    while (c<48||c>57)  c=getchar();
    while (c>=48&&c<=57)    p=(p<<1)+(p<<3)+c-48,c=getchar();
    return p;
}

inline void Maketree(int x){
    int i=0,t=s[x].size(),w=0;
    if (v[x])   return;
    v[x]=1; id[x]=++id[0];
    for (i=0;i<t;i++){
        w=s[x][i];
        if (!v[w]){
            S[x].push_back(w);  d[w]=d[x]+1;
            f[w][0]=x;  Maketree(w);
        }
    }
}

inline void Dfs(int x){
    int i=0,t=S[x].size(),w=0;
    for (i=1;i<=20;i++){
        f[x][i]=f[f[x][i-1]][i-1];
    }
    for (i=0;i<t;i++)   Dfs(S[x][i]);
}

inline void Ready(){
    int i=0;
    for (i=0;i<=20;i++) p[i]=1<<i;
    d[1]=1; Maketree(1);    Dfs(1);
    id[0]=z[0]=0;
}

inline int LCA(int x,int y){
    int i=0,l=0;
    if (d[x]<d[y])  Swap(x,y);
    l=d[x]-d[y];    L=0;
    for (i=0;i<=20;i++)
        if (l&p[i]) L+=p[i],x=f[x][i];
    if (x==y)   return x;
    for (i=20;i>=0;i--)
        if (f[x][i]!=f[y][i])   x=f[x][i],y=f[y][i];
    return f[x][0];
}

inline void Add(int s,int t){
    LCA(s,t);
    e[lsum].t=s;    e[lsum].d=L;    e[lsum].next=head[t];   head[t]=lsum++;
}

inline void Update(int x){
    int i=0,t=0;
    size[x]=0;
    if (b[x]==n+q)  size[x]=1,cnt++;
    for (i=head[x];i;i=e[i].next){
        t=e[i].t;   Update(t);  size[x]+=size[t];
    }
}

inline void Dp(int x){
    int i=0,t=0;
    LL l=0;
    if (b[x]==n+q)  dp[x][0]=dp[x][1]=0;
    else dp[x][0]=-INF,dp[x][1]=INF;
    for (i=head[x];i;i=e[i].next){
        t=e[i].t;   Dp(t);  l=e[i].d;
        Ans+=(LL)l*((LL)cnt-size[t])*(LL)size[t];
        mxans=Max(mxans,dp[x][0]+l+dp[t][0]);
        mians=Min(mians,dp[x][1]+l+dp[t][1]);
        dp[x][0]=Max(dp[x][0],dp[t][0]+l);
        dp[x][1]=Min(dp[x][1],dp[t][1]+l);
    }
}

inline void Work(){
    int i=0,p=0;
    sort(a+1,a+1+k,cmp);
    z[top=1]=1; lsum=1; head[1]=0;
    for (i=1;i<=k;i++){
        if (a[i]==1)    continue;
        p=LCA(a[i],z[top]);
        if (p!=z[top]){
            while (id[p]<id[z[top-1]])  Add(z[top],z[top-1]),top--;
            if (p!=z[top-1])    head[p]=0,Add(z[top],p),z[top]=p;
            else Add(z[top--],p);
        }
        z[++top]=a[i];  head[a[i]]=0;
    }
    for (i=1;i<top;i++) Add(z[i+1],z[i]);
    cnt=0;  Ans=0;  mians=INF;  mxans=-INF;
    Update(1);  Dp(1);
    printf("%lld %d %d\n",Ans,mians,mxans);
}

int main(){
    n=read();
    for (i=1;i<n;i++){
        x=read();   y=read();
        s[x].push_back(y);  s[y].push_back(x);
    }
    Ready();    q=read();
    while (q--){
        k=read();
        for (i=1;i<=k;i++)  a[i]=read(),b[a[i]]=n+q;
        Work();
    }
    return 0;
}
说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...