虚树是用来优化树形dp的东西,它的转移是从一些特殊点,向根节点转移,期间它有用的转移点比较特殊。通常询问次数较多,但特殊点总和较少,就可以每次询问先建虚树再跑dp。单调栈建虚树 O ( k l o g n ) O(klogn) O(klogn), k k k为特殊点数, n n n为原树上点数
虚树的点数为特殊点两倍
单调栈构造虚树,强制根节点为1(或者先加入dfs最小的特殊点),便于统计答案
void build(){
std::sort(a+1,a+1+m,[&](int i,int j){
return dfn[i]<dfn[j];
});
int top=0;
cnt=0;
stk[++top]=1;
head[1]=0;//顺便清空之前的虚树,也可以直接遍历一遍虚树清除
for (int i=1;i<=m;i++){
if (a[i]!=1){
//全程不用管top大小
int lc=lca(stk[top],a[i]);
if (lc!=stk[top]){//当前节点已不再当前栈维护的链上
while (dfn[lc]<dfn[stk[top-1]]){//一直跳出,并添加已经确定的边
add(stk[top-1],stk[top],0);
top--;
}
if (dfn[lc]>dfn[stk[top-1]]){//lc之前没有加入到栈中
head[lc]=0;
add(lc,stk[top--],0);
stk[++top]=lc;
}else{//lc加入过栈
add(lc,stk[top--],0);
}
}
//最后只要把a[i]加入该链
head[a[i]]=0;
stk[++top]=a[i];
}
}
for (int i=1;i<top;i++){
add(stk[i],stk[i+1],0);//将链上的节点依次建边
}
}
P2495 [SDOI2011] 消耗战
正常的每次询问跑一边树形dp时间爆炸,所以直接建虚树跑树形dp,在原树上要维护一个点
x
x
x 到根节点的最小断边
f
x
f_x
fx。转移直接分情况:
当前点为特殊点,必须将
u
u
u分离出来,
d
p
u
=
f
u
dp_u=f_u
dpu=fu
当前点不为特殊点,可以将
u
u
u直接分离,或者让
u
u
u从其已经分离的子树分离,
d
p
u
=
m
i
n
(
f
u
,
∑
d
p
v
)
dp_u=min(f_u,\sum dp_v)
dpu=min(fu,∑dpv)
CF613D Kingdom and its Cities
如果有一条边连接的都是重要城市,输出-1
建完虚树,比较暴力的跑就是
d
p
u
,
0
/
1
dp_{u,0/1}
dpu,0/1表示
u
u
u子树合法,与
u
u
u连通的块中无/有一个特殊点,所需的最小代价
对于
u
u
u为特殊点,很显然只能转移到
d
p
u
,
1
dp_{u,1}
dpu,1,和孩子节点全分开
d
p
u
,
1
=
∑
(
m
i
n
(
d
p
v
,
0
,
d
p
v
,
1
)
+
1
)
dp_{u,1}=\sum (min(dp_{v,0},dp_{v,1})+1)
dpu,1=∑(min(dpv,0,dpv,1)+1)
对于
u
u
u不为特殊点,
d
p
u
,
0
dp_{u,0}
dpu,0转移同上
d
p
u
,
1
dp_{u,1}
dpu,1就是特意留下一个
d
p
v
,
1
dp_{v,1}
dpv,1转移过来且不断开(这里转移就很不好搞了)
我们发现一个贪心性质,就是能留特殊点,就尽量留下来,所以我们就可以省一维,然后多开一个数组 f u f_u fu来存储包含 u u u的连通块是否包含特殊点,等到向父亲传递转移时,迫不得已再断开
CF1111E Tree
先按照原树dfs序建虚树(根节点不再是1,选择特殊点中dfs序最小的那个,一定要加入
r
r
r),然后换虚树根跑dp
dp状态部分很平常,因为它是分了组的,联想到第二类斯特林的递推式。
d
p
u
,
j
=
(
j
−
b
a
d
u
)
∗
d
p
u
,
j
+
d
p
u
,
j
−
1
dp_{u,j}=(j-bad_u)*dp_{u,j}+dp_{u,j-1}
dpu,j=(j−badu)∗dpu,j+dpu,j−1,
b
a
d
u
bad_u
badu表示根节点到
u
u
u路径上特殊点个数
放入已有组,或新开一个组单独放入,遍历顺序我们只需保证在遍历到
u
u
u时,
b
a
d
u
bad_u
badu已经求出来了即可,常规dfs就可以了