LGP2495 [SDTS 2011] 消耗战 学习笔记
LGP2495 [SDTS 2011] 消耗战 学习笔记
前言
从这题入门虚树吧。
题意简述
给定一棵 \(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;
}```
浙公网安备 33010602011771号