P2495 [SDOI2011]消耗战

传送门

虚树DP经典题

首先有一个显然的$O(nm)$的树形DP

以 1 号节点为根

设 $f [ x ]$ 表示把节点 $x$ 子树内的资源点都与 $x$ 的父节点断开的最小代价

那么转移显然:

枚举 $x$ 的所有儿子节点 $v$,设 $x$ 到父节点的边权为 $w$

$f [ x ] = min ( w,sum_vf[v] )$

然后发现 $sum_{k_i}leq 500000$

所以显然要搞虚树

每次DP只要把有用的节点提出来,根据它们之间的父子关系建一颗虚树

DP只要在虚树上DP就可以求出答案

对于虚树上的边权容易想到取原树上对应的一段路径的边权最小值,也容易想到倍增求这个最小值

但是我们可以贪心一下来减少代码难度:

直接维护从当前节点到根的边权最小值,如果最小值在路径就跟上面的没区别

如果在更离根更近的位置上显然删掉此边会比删路径上的最小值更优

然后就可以构建虚树了,构造过程就不细讲了

求LCA我用的是ST表

总复杂度 $O(sum_{k_i}+nlog_n)$

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
inline int read()
{
    int x=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
    return x*f;
}
const int N=5e5+7;
const ll INF=1e16+7;
int fir[N],from[N<<1],to[N<<1],val[N<<1],cntt;//存原树
inline void add(int &a,int &b,int &c)
{
    from[++cntt]=fir[a]; fir[a]=cntt;
    to[cntt]=b; val[cntt]=c;
}
int n,m;
int st[N<<1],Top,dfn[N],cnt,pos[N<<1];//pos存节点第一次在欧拉序中出现的位置,用于ST表求LCA
ll mi[N];//存节点到根的边权最小值
void dfs(int x,int fa)//dfs预处理各种东西
{
    dfn[x]=++cnt;
    st[++Top]=x; pos[x]=Top;
    for(int i=fir[x];i;i=from[i])
    {
        int &v=to[i]; if(v==fa) continue;
        mi[v]=min(mi[x],1ll*val[i]);//先更新mi
        dfs(v,x); st[++Top]=x;//再dfs下去,出来后x要再入一次栈
    }
}
int f[N<<1][21],Log[N<<1];//ST表
void pre()//预处理ST表
{
    Log[0]=-1; for(int i=1;i<=Top;i++) Log[i]=Log[i>>1]+1;
    for(int i=1;i<=Top;i++) f[i][0]=st[i];
    for(int i=1;(1<<i)<=Top;i++)
    {
        for(int j=1;j+(1<<i-1)<=Top;j++)
        {
            if(dfn[f[j][i-1]]<dfn[f[j+(1<<i-1)][i-1]]) f[j][i]=f[j][i-1];
            else f[j][i]=f[j+(1<<i-1)][i-1];
        }
    }
}
inline int query(int x,int y)//求节点x,y的LCA
{
    int l=pos[x],r=pos[y],k;
    if(l>r) swap(l,r);
    k=Log[r-l+1];
    if(dfn[f[l][k]]<dfn[f[r-(1<<k)+1][k]]) return f[l][k];
    else return f[r-(1<<k)+1][k];
}
vector <int> v[N];//vector存虚树
inline void Add(int x,int y) { v[x].push_back(y); }//虚树上连边
inline void ins(int x)//一个个插入构造虚树
{
    if(Top==1) { st[++Top]=x; return; }
    int lca=query(x,st[Top]);
    if(lca==st[Top]) return;//可以发现如果x与st[Top]在同一条链上那么x就没用了,因为st[Top]上面肯定要割边
    while(Top>1 && dfn[st[Top-1]]>=dfn[lca]) Add(st[Top-1],st[Top]),Top--;//如果处理完一条链就加入虚树并弹出节点
    if(lca!=st[Top]) Add(lca,st[Top]),st[Top]=lca;//考虑把lca入栈
    st[++Top]=x;//最后加入x
}
ll dp(int x)//虚树上DP
{
    int len=v[x].size();
    if(!len) return mi[x];
    ll sum=0;
    for(int i=0;i<len;i++)
        sum+=dp(v[x][i]);
    v[x].clear();//记得DP完把虚树删掉
    return min(sum,mi[x]);
}
int t,d[N];
inline bool cmp(const int &a,const int &b) { return dfn[a]<dfn[b]; }//按dfn排序
int main()
{
    n=read(); int a,b,c;
    for(int i=1;i<n;i++)
    {
        a=read(),b=read(),c=read();
        add(a,b,c); add(b,a,c);
    }
    mi[1]=INF; dfs(1,1);
    pre(); m=read();
    for(int i=1;i<=m;i++)
    {
        t=read();
        for(int j=1;j<=t;j++) d[j]=read();
        sort(d+1,d+t+1,cmp); st[Top=1]=1;
        for(int j=1;j<=t;j++) ins(d[j]);//一个个加入
        while(Top) Add(st[Top-1],st[Top]),Top--;//最后还有一条链没加入虚树
        printf("%lld
",dp(1));
    }
    return 0;
}
原文地址:https://www.cnblogs.com/LLTYYC/p/10199860.html