虚树(virtual tree)的概念
虚树 是将一个树的点集的某一个子集,以及该子集中点的 L C A LCA LCA 的集合,一起所重构出来的一棵树
虚树的用途
在树型dp中,有时候没必要对整颗树进行dp,只用对某个子集构成的虚树进行dp,大大降低了时空复杂度
例题:P2495 [SDOI2011] 消耗战
[SDOI2011] 消耗战
题目描述
在一场战争中,战场由 n n n 个岛屿和 n − 1 n-1 n−1 个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为 1 1 1 的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他 k k k 个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到 1 1 1 号岛屿上)。不过侦查部门还发现了这台机器只能够使用 m m m 次,所以我们只需要把每次任务完成即可。
输入格式
第一行一个整数 n n n,表示岛屿数量。
接下来 n − 1 n-1 n−1 行,每行三个整数 u , v , w u,v,w u,v,w ,表示 u u u 号岛屿和 v v v 号岛屿由一条代价为 w w w 的桥梁直接相连。
第 n + 1 n+1 n+1 行,一个整数 m m m ,代表敌方机器能使用的次数。
接下来 m m m 行,第 i i i 行一个整数 k i k_i ki ,代表第 i i i 次后,有 k i k_i ki 个岛屿资源丰富。接下来 k i k_i ki 个整数 h 1 , h 2 , . . . , h k i h_1,h_2,..., h_{k_i} h1,h2,...,hki ,表示资源丰富岛屿的编号。
输出格式
输出共 m m m 行,表示每次任务的最小代价。
样例 #1
样例输入 #1
10
1 5 13
1 9 6
2 1 19
2 4 8
2 3 91
5 6 8
7 5 4
7 8 31
10 7 9
3
2 10 6
4 5 7 8 3
3 9 4 6
样例输出 #1
12
32
22
提示
数据规模与约定
- 对于 10 % 10\% 10% 的数据, n ≤ 10 , m ≤ 5 n\leq 10, m\leq 5 n≤10,m≤5 。
- 对于 20 % 20\% 20% 的数据, n ≤ 100 , m ≤ 100 , 1 ≤ k i ≤ 10 n\leq 100, m\leq 100, 1\leq k_i\leq 10 n≤100,m≤100,1≤ki≤10 。
- 对于 40 % 40\% 40% 的数据, n ≤ 1000 , 1 ≤ k i ≤ 15 n\leq 1000, 1\leq k_i\leq 15 n≤1000,1≤ki≤15 。
- 对于 100 % 100\% 100% 的数据, 2 ≤ n ≤ 2.5 × 1 0 5 , 1 ≤ m ≤ 5 × 1 0 5 , ∑ k i ≤ 5 × 1 0 5 , 1 ≤ k i < n , h i ≠ 1 , 1 ≤ u , v ≤ n , 1 ≤ w ≤ 1 0 5 2\leq n \leq 2.5\times 10^5, 1\leq m\leq 5\times 10^5, \sum k_i \leq 5\times 10^5, 1\leq k_i< n, h_i\neq 1, 1\leq u,v\leq n, 1\leq w\leq 10^5 2≤n≤2.5×105,1≤m≤5×105,∑ki≤5×105,1≤ki<n,hi=1,1≤u,v≤n,1≤w≤105 。
虚树的构建
虚树的重要性质:祖先后代的关系没有改变,是关键点的浓缩子树,例如下图所示(来自oiwiki)
以上述题为例子
第一步:初始化dfs序,f数组,g数组
对树上的每个节点,打上dfs序标记,构建 L C A LCA LCA 的树上倍增 f f f 数组,这里有个小技巧,就是查询树上两点之间的简单路径的 最小/最大 权值,利用 g g g 数组:
1.初始化 g g g 数组:
rep(j,1,K) rep(i,1,n) g[i][j]=min(g[i][j-1],g[f[i][j-1]][j-1]);
2.树上倍增法,查询祖先节点与子孙节点之间的权值的最值情况
inline int getw(int x,int y)
{
if(d[x]>d[y]) swap(x,y);
int ans=INF,i;
repf(i,K,0) if(d[f[y][i]]>=d[x]) ans=min(ans,g[y][i]),y=f[y][i];
return ans;
}
void getrd(int u,int fa,int h) //求dfs序
{
int v,e;
rnk[u]=idx++; //dfs序
d[u]=h; //深度数组,用于 LCA 树上倍增法
for(e=head[u];e;e=table[e].nxt)
{
v=table[e].to;
if(v==fa) continue;
f[v][0]=u; //初始化 f 数组
g[v][0]=table[e].val; //初始化 g 数组
getrd(v,u,h+1);
}
}
第二步:构建虚树(核心)
- 首先将1号节点,即根节点加入栈中,(即使不是关键点,但更加统一化)
- 主体循环是从小到大(dfs序)遍历关键点序列,如下图理解:
void buildtr() //建虚树
{
int i,j,f,w,tph;
q[hh]=1,pp[hh++]={rnk[1],1};
k=read();
rep(i,1,k) q[hh]=read(),pp[hh]={rnk[q[hh]],q[hh]},isk[q[hh]]=true,hh++;
sort(pp,pp+hh);
st[ptr++]=1; //st为栈
tph=hh-1;
rep(f,1,tph) //i为当前即将入栈的节点
{
//初始化变量
i=pp[f].s;
tt=0;
//弹栈
auto anc=lca(i,st[ptr-1]);
while(rnk[st[ptr-1]]>rnk[anc]) out[tt++]=st[--ptr];
if(rnk[st[ptr-1]]<rnk[anc]) st[ptr++]=anc,q[hh++]=anc;
out[tt++]=st[ptr-1];
//入栈
st[ptr++]=i;
//链接
rep(j,0,tt-2) w=getw(out[j],out[j+1]),add_(out[j],out[j+1],w),add_(out[j+1],out[j],w);
}
//最终链接
rep(j,0,ptr-2) w=getw(st[j],st[j+1]),add_(st[j],st[j+1],w),add_(st[j+1],st[j],w);
}
第三步:dp
时间复杂度分析:
构建LCA-ST的复杂度:
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
构建虚树的时间复杂度:
O
(
K
l
o
g
N
)
O(KlogN)
O(KlogN) ,其中
K
K
K为关键点的数量
总体代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
#include<string>
#include<set>
#include<map>
#include<unordered_map>
#include<queue>
#define me(x,y) memset(x,y,sizeof x)
#define rep(i,x,y) for(i=x;i<=y;++i)
#define repf(i,x,y) for(i=x;i>=y;--i)
#define lowbit(x) -x&x
#define inf 0x3f3f3f3f
#define INF 0x7fffffff
#define f first
#define s second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
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*10+ch-48;ch=getchar();}
return x*f;
}
struct edge
{
int to;
int nxt;
int val;
};
const int N= 250010,K=20;
int n,m,k,cnt,cnt_,idx,hh,tt,ptr;
int head[N],head_[N],d[N],q[N],rnk[N],st[N],out[N],f[N][K+3],g[N][K+3];
ll dp[N];
edge table[N*2],table_[N*2];
bool isk[N];
PII pp[N];
inline void add(int& u,int& v,int& w)
{
table[++cnt].nxt=head[u];
head[u]=cnt;
table[cnt].to=v;
table[cnt].val=w;
}
inline void add_(int& u,int& v,int& w)
{
table_[++cnt_].nxt=head_[u];
head_[u]=cnt_;
table_[cnt_].to=v;
table_[cnt_].val=w;
}
inline int lca(int x,int y) //求lca
{
int i;
if(d[x]>d[y]) swap(x,y);
repf(i,K,0) if(d[f[y][i]]>=d[x]) y=f[y][i];
if(x==y) return x;
repf(i,K,0) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
inline int getw(int x,int y)
{
if(d[x]>d[y]) swap(x,y);
int ans=INF,i;
repf(i,K,0) if(d[f[y][i]]>=d[x]) ans=min(ans,g[y][i]),y=f[y][i];
return ans;
}
void buildf() //建立f表
{
int i,j;
rep(j,1,K) rep(i,1,n) f[i][j]=f[f[i][j-1]][j-1],g[i][j]=min(g[i][j-1],g[f[i][j-1]][j-1]);
}
void getrd(int u,int fa,int h) //求dfs序
{
int v,e;
rnk[u]=idx++;
d[u]=h;
for(e=head[u];e;e=table[e].nxt)
{
v=table[e].to;
if(v==fa) continue;
f[v][0]=u;
g[v][0]=table[e].val;
getrd(v,u,h+1);
}
}
void m_clear() //清理
{
int i;
rep(i,0,hh-1) isk[q[i]]=head_[q[i]]=dp[q[i]]=0;
cnt_=hh=tt=ptr=0;
}
void buildtr() //建虚树
{
int i,j,f,w,tph;
q[hh]=1,pp[hh++]={rnk[1],1};
k=read();
rep(i,1,k) q[hh]=read(),pp[hh]={rnk[q[hh]],q[hh]},isk[q[hh]]=true,hh++;
sort(pp,pp+hh);
st[ptr++]=1; //st为栈
tph=hh-1;
rep(f,1,tph) //i为当前即将入栈的节点
{
//初始化变量
i=pp[f].s;
tt=0;
//弹栈
auto anc=lca(i,st[ptr-1]);
while(rnk[st[ptr-1]]>rnk[anc]) out[tt++]=st[--ptr];
if(rnk[st[ptr-1]]<rnk[anc]) st[ptr++]=anc,q[hh++]=anc;
out[tt++]=st[ptr-1];
//入栈
st[ptr++]=i;
//链接
rep(j,0,tt-2) w=getw(out[j],out[j+1]),add_(out[j],out[j+1],w),add_(out[j+1],out[j],w);
}
//最终链接
rep(j,0,ptr-2) w=getw(st[j],st[j+1]),add_(st[j],st[j+1],w),add_(st[j+1],st[j],w);
}
void getdp(int u,int fa)
{
int e,v;
for(e=head_[u];e;e=table_[e].nxt)
{
v=table_[e].to;
if(v==fa) continue;
getdp(v,u);
if(isk[v]) dp[u]+=table_[e].val;
else dp[u]+=min((ll)table_[e].val,dp[v]);
}
}
int main()
{
int i,j,u,v,w;
n=read();
rep(i,1,n-1)
{
u=read(),v=read(),w=read();
add(u,v,w),add(v,u,w);
}
getrd(1,1,1);
buildf();
m=read();
while(m--)
{
buildtr();
getdp(1,1);
printf("%lld\n",dp[1]);
m_clear();
}
return 0;
}