虚树是用来优化树形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就可以了