LGP2495 [SDTS 2011] 消耗战 学习笔记

LGP2495 [SDTS 2011] 消耗战 学习笔记

Luogu Link

前言

从这题入门虚树吧。

题意简述

给定一棵 \(n\) 个结点的有权树,\(1\) 为根。\(m\) 次询问。每次会给定 \(k_i\) 个关键点。你可以花费其权值的代价割掉一条边,问能使得 \(1\) 不与任何关键点连通所需的最小代价。

\(n\le 2.5\times 10^5\)\(\sum k_i=5\times 10^5\)。保证 \(1\) 不是关键点。

做法解析

我们需要虚树

首先,如果只有单次询问,这就是个非常朴素的树形 \(\texttt{DP}\)

具体来说,设 \(dp_u\) 为能使 \(u\) 的子树内(包含 \(u\) 自身)不存在任何能与 \(u\) 连通的关键点所需的最小代价。\(dp_u=\sum \min(dp_v,w_{u\to v})\)。其中,当 \(u\) 为关键点时要令 \(dp_u=\infty\)。答案即 \(dp_1\)

然而这是多次询问。怎么办呢。

其实你能感受到每次的 \(\texttt{DP}\) 做了很多重复工作,另外,关键点的总数是有限制的。要是我们的复杂度能做到 \(O(\sum k_i)\) 左右就好了。有什么办法吗?

有的兄弟有的。我们对于每个询问建一棵“虚树”就行了。

什么是虚树?

“虚树”就是只包含原树上关键点和关键点之间的 \(\text{lca}\) 的树。

它对我们的复杂度有多大的帮助?

(可以理解和证明,)一个新加入的 \(\text{lca}\) 意味着“合并两个关键点”,因此其最多合并 \(n-1\) 次,所以虚树的点数是 \(2n\) 级别的。

那虚树怎么建?

建树法一

第一种方法属于不用动脑的大常熟做法。

bool cmpd(int x,int y){return dfn[x]<dfn[y];}
void vtbuild(int k){
    sort(P+1,P+k+1,cmpd);
    for(int i=1;i<=k;i++)V[i]=P[i];
    for(int i=1;i<k;i++)V[i+k]=getlca(P[i],P[i+1]);
    V[k*2]=1,sort(V+1,V+k*2+1,cmpd);vcnt=unique(V+1,V+k*2+1)-(V+1);
    for(int i=1;i<vcnt;i++)addedge(getlca(V[i],V[i+1]),V[i+1]);
}

(注:为了统计答案方便,我们强制 \(1\) 包含于虚树)

我们先把关键点按照 \(\text{dfs}\) 序排序,然后加入它们会带来的 \(\text{lca}\)。可以证明对 \(n\) 个点进行这种操作只会带来最多 \(n-1\) 个这种 \(\text{lca}\),且一定可以通过遍历 \(\text{lca}(p_i,p_{i+1})\) 求到。

这个结论我们简单证明下:

首先一个基本事实:对于 \(a_i=\text{lca}(u,x_i)\),它们一定都在从 \(u\) 到根的直链上。

我们要证明的其实就是:不存在 \(p_i,p_{i+k}\) 使得 \(\text{lca}(p_i,p_{i+k})\) 不在 \(\text{lca}(p_j,p_{j+1}),j\in[i,i+k-1)\) 构成的集合中。

为什么呢?以 \(k=2\) 为例。我们设 \(a=p_i,b=p_{i+1},c=p_{i+2}\)。那么,\(\text{lca}(a,b)\)\(\text{lca}(b,c)\) 肯定都在 \(b\) 往根走的直链上。而 \(\text{lca}(a,c)\)\(a\)\(c\) 往根走的直链上,又:\(a\) 的直链与 \(b\) 交汇于 \(\text{lca}(a,b)\)\(c\) 的同理。

不妨设 \(\text{lca}(a,b)\)\(\text{lca}(b,c)\) 更靠近根,那么在 \(\text{lca}(a,b)\) 处,\(a,b,c\) 往上的直链就都交汇了,说明 \(\text{lca}(a,c)\) 此时就等于 \(\text{lca}(a,b)\)

你可能有疑问:万一 \(\text{lca}(a,c)\)\(\text{lca}(a,b)\) 的子树下面呢?实际上因为我们保证了 \(a,b,c\)\(\text{dfs}\) 序单增,你再考虑 \(\text{dfs}\) 的遍历顺序,就可以知道这不可能。

推广到 \(k>2\) 也是容易的,原理一致。

以及,另外一个结论“一个点集的 \(\text{lca}\) 等于 \(\text{lca}(p_i,p_{i+1})\) 的最浅者”与这个结论的证明原理其实有一些内在共通之处。

这样我们就得到了所有虚树上的点,记为 \(V\)。我们把这所有的点再按 \(\text{dfs}\) 序排序一下,然后从 \(i=1\) 开始,连形如 \(\text{lca}(v_i,v_{i+1})\to v_{i+1}\) 的边,(我们的实现显然会有 \(v_1=1\),所以 \(v_1\) 不用被连边,)由于我们是按照 \(\text{dfs}\) 序做的,所以这是对的。

建树法二

我们有一种用单调栈建立虚树的写法。这种写法在某个意义上属于用可读性换效率。因为法一要两遍排序一遍 unique,常熟太大。

void vtbuild(int k){
    sort(P+1,P+k+1,cmpd),stk[ktp=1]=1;
    for(int i=1;i<=k;i++){
        if(P[i]==1)continue;int ca=getlca(P[i],stk[ktp]);
        if(ca!=stk[ktp]){
            while(dfn[ca]<dfn[stk[ktp-1]])addedge(stk[ktp-1],stk[ktp]),ktp--;
            if(dfn[ca]!=dfn[stk[ktp-1]])addedge(ca,stk[ktp]),stk[ktp]=ca;
            else addedge(ca,stk[ktp--]);
        }
        stk[++ktp]=P[i];
    }
    for(int i=1;i<ktp;i++)addedge(stk[i],stk[i+1]);
}

这种写法的本质就是用单调栈维护一条链,然后在结点被出栈的时候添加它父亲到它的边。

你可能想问为什么不能在入栈的时候加边。这是因为之后的点带来的 \(\text{lca}\) 可能会拦在当前某点和它的父亲中间。

注意到 while(dfn[ca]<dfn[stk[ktp-1]])。这意味着如果碰到新造成的 \(\text{lca}\),我们希望在栈中还保留它在原链上的儿子,即 stk[ktp],而从 stk[ktp-1] 开始余下的皆是其祖先,来对问题进行考量。

我们不把 \(1\) 加入 P 数组,而是另外地先行加入。

关于边权

那虚树的边权呢?

你可能会想着,虚树上 \((u,v)\) 之间的边权值等于原树上 \((u,v)\) 间所有边权中的最小值。但实际上这没必要。我们直接令它等于原树上 \(1\)\(v\) 之间所有边权的最小值即可。为什么这是对的呢?因为如果一条边更浅的结点处于另一条边更深的结点的子树当中,而后者的权值又比前者更小,那我们肯定切后者啊,既靠上又省钱。于此,前者的边权也可以设定为与后者一样,反正我们不会切到它,把它这么改不影响求最优解。

代码实现

#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=2.5e5+5,Inf1=0x3f3f3f3f;
const lolo Inf2=1e18;
int N,X,Y,Z,M,K;
struct edge{int to,co;};
vector<edge> Tr[MaxN];
void addudge(int u,int v,int w){
    Tr[u].push_back({v,w});
    Tr[v].push_back({u,w});
}
int tfa[MaxN],dep[MaxN],siz[MaxN],hvs[MaxN],mnv[MaxN];
void dfs1(int u,int f){
    dep[u]=dep[f]+1,siz[u]=1,tfa[u]=f;
    for(auto [v,w] : Tr[u]){
        if(v==f)continue;
        mnv[v]=min(mnv[u],w),dfs1(v,u),siz[u]+=siz[v];
        if(siz[v]>siz[hvs[u]])hvs[u]=v;
    }
}
int dfn[MaxN],dcnt,top[MaxN];
void dfs2(int u,int t){
    top[u]=t,dfn[u]=++dcnt;
    if(hvs[u])dfs2(hvs[u],t);
    for(auto [v,w] : Tr[u]){
        if(v==tfa[u]||v==hvs[u])continue;
        dfs2(v,v);
    }
}
int getlca(int x,int y){
    for(;top[x]!=top[y];){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        x=tfa[top[x]];
    }
    return dep[x]<=dep[y]?x:y; 
}
vector<int> Vr[MaxN];
void addedge(int u,int v){Vr[u].push_back(v);}
int isk[MaxN],P[MaxN],stk[MaxN],ktp;
bool cmpd(int x,int y){return dfn[x]<dfn[y];}
void vtbuild(int k){
    sort(P+1,P+k+1,cmpd);
    stk[ktp=1]=1;
    for(int i=1;i<=k;i++){
        if(P[i]==1)continue;int ca=getlca(P[i],stk[ktp]);
        if(ca!=stk[ktp]){
            while(dfn[ca]<dfn[stk[ktp-1]])addedge(stk[ktp-1],stk[ktp]),ktp--;
            if(dfn[ca]!=dfn[stk[ktp-1]])addedge(ca,stk[ktp]),stk[ktp]=ca;
            else addedge(ca,stk[ktp--]);
        }
        stk[++ktp]=P[i];
    }
    for(int i=1;i<ktp;i++)addedge(stk[i],stk[i+1]);
}
lolo dp[MaxN];
void DP(int u){
    dp[u]=0;
    for(int v : Vr[u])DP(v),dp[u]+=min(dp[v],(lolo)mnv[v]);
    Vr[u].clear();if(isk[u])dp[u]=Inf2;
}
int main(){
    readi(N);
    for(int i=1;i<N;i++)readis(X,Y,Z),addudge(X,Y,Z);
    mnv[1]=Inf1,dfs1(1,0),dfs2(1,1),readi(M);
    for(int i=1;i<=M;i++){
        readi(K);
        for(int j=1;j<=K;j++)readi(P[j]),isk[P[j]]=1;
        vtbuild(K);DP(1),writil(dp[1]);
        for(int j=1;j<=K;j++)isk[P[j]]=0;
    }
    return 0;
}```
posted @ 2025-07-31 10:36  矞龙OrinLoong  阅读(7)  评论(0)    收藏  举报