[图论入门]虚树
不知道虚树到底应该放到 “算法” 里还是 “图论” 里...暂且当时图论里的吧。
#1.0 虚树
#1.1 简单介绍
虚树,是对于一棵给定的节点数为 \(n\) 的树 \(T\),构造一棵新的树 \(T'\) 使得总结点数最小且包含指定的某几个节点和他们的 \(\texttt{LCA}\)。
一般用来优化树形 \(\texttt{DP}\) 等。比如说,有一颗 \(n\) 个节点的树,只有 \(m\) 个节点必须要考虑(\(m\) 可能远小于 \(n\)),那么我们完全可以不去考虑那些没有任何用处的节点。像是树形 \(\texttt{DP}\),我们产生贡献、积累贡献的地方有可能只有关键点与他们两两之间的 \(\texttt{LCA}\),那么我们只需要存下关键点与 \(\texttt{LCA}\) 即可。
#1.2 实现思路
首先,将关键点按 \(\texttt{dfn}\) 序排序。然后我们需要一条单调栈来维护一条虚树上的链,栈中的 \(\texttt{dfn}\) 序单调递增。这意味着,栈中相邻的节点在虚树上也是相邻的,且栈中一个节点屁股底下的就是自己在虚树中的父亲节点。
首先,为了方便后面处理,我们先强制把根节点加入栈中,那么下面将依次将关键点加入栈。
第一种情况:当前节点与 stack[top]
的 \(\texttt{LCA}\) 就是 stack[top]
,那么说明他们在同一条链上,可以直接加入栈;
第二种情况:当前节点与栈顶元素的 \(\texttt{LCA}\) 不是栈顶元素,那么说明他们不是同一条链上的(分叉了),将栈中的节点弹出并连边,直到 stack[top - 1]
的深度比求得的 \(\texttt{LCA}\) 的深度要小,那么说明异端岔开的链已经只剩栈顶一个了,检查 stack[top - 1]
是否是 \(\texttt{LCA}\),如果不是,就需要将 \(\texttt{LCA}\) 向栈顶连一条边,然后将栈顶弹出,加入 \(\texttt{LCA}\)。就可以将该节点加入栈了。
最后将栈中的节点全部弹出并连边就好了。
注意一点,在虚树建立后,进行操作的最后一个 \(\texttt{DFS}\) 的过程中,要清空邻接表,但不要直接暴力 memset()
,那样会使复杂度退化,可以直接将头指针清空,如果是 vector
可以直接 erase()
。
#1.3 代码实现
inline void ins(int x){ //插入某个关键点
if (!stp) {st[++ stp] = x;return;} //空栈,直接插入
int ance = LCA(x,st[stp]);
/*不在同一条链上的要弹出,在同一条链上的不会进入循环*/
while (stp > 1 && d[ance] < d[st[stp - 1]])
add(st[stp - 1],st[stp]),stp --;
if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
if (!stp || st[stp] != ance) st[++ stp] = ance;
st[++ stp] = x;
}
#2.0 例题
#2.1 P2495 [SDOI2011]消耗战
普通的树形 \(\texttt{DP}\) 并不难想,对于一个树上的非关键点 \(x\),只有断掉它连向父亲的边和断掉连向子孙的边两种情况。断上面的情况可以在其父节点处理,所以只用考虑断连向子孙的边。如果 \(x\) 的一个儿子是关键点,那么这条边必断,如果一个儿子不是关键点,考虑是断这个儿子上面还是下面,取最小值。
但是直接做时间复杂度爆炸,注意到 \(\sum k_i\leq5\times10^5\),再发现有许多点没有用处,考虑建虚树处理。
建完虚树后,原来的 \(\texttt{DP}\) 策略似乎不太可行了,注意到在虚树上的点除了关键点就是他们的 \(\texttt{LCA}\),且叶节点均为关键点。
发现,如果一个点是关键点,那么,在它下面的关键点可以不加入虚树,因为在该点之上必然存在删除的边,从而导致下面的点也不联通。
按上面的想法建出虚树,不妨设 \(f(x)\) 表示在虚树上以 \(x\) 为根的子树(不包含 \(x\))中的所有关键点与 \(1\) 号节点断开需要的最小代价,显然有
其中 \(mn(x)\) 表示从 \(x\) 到 \(1\) 号节点路径上的最小边权。现在能有子节点的只有 \(\texttt{LCA}\) 了。考虑为什么可以取 \(mn(x)\),对于 \(x\) 的父节点 \(fa_x\),如果连向 \(x\) 的边 \((fa_x,x)\) 的边权大于 \(mn(x)\),那么由于和式其余项相加大于等于 \(0\);所以必然仍取 \(mn(fa_x)\),如果 \((fa_x,x)=mn(x)\),正确性显然。
const int N = 2000010;
const int INF = 0x3fffffff;
struct Edge{
int u,v;
ll w;
int nxt;
};
Edge e[N],ne[N];
int cnt = 1,head[N],n,m,ncnt = 1,nhead[N];
int son[N],dfn[N],T,st[N],stp,f[N];
int fa[N],size[N],d[N],top[N],fl[N];
ll mn[N];
inline ll Min(const ll &a,const ll &b){
return a < b ? a : b;
}
inline void ADD(const int &u,const int &v,const ll &w){
e[cnt].u = u;e[cnt].v = v;e[cnt].w = w;
e[cnt].nxt = head[u];head[u] = cnt ++;
}
inline void add(const int &u,const int &v){
ne[ncnt].u = u;ne[ncnt].v = v;
ne[ncnt].nxt = nhead[u];nhead[u] = ncnt ++;
}
inline int cmp(const int &a,const int &b){
return dfn[a] < dfn[b];
}
inline void dfs1(int x,int _fa){
size[x] = 1,d[x] = d[_fa] + 1,fa[x] = _fa;
for (int i = head[x];i;i = e[i].nxt){
if (e[i].v == fa[x]) continue;
mn[e[i].v] = Min(mn[x],e[i].w);
dfs1(e[i].v,x);
size[x] += size[e[i].v];
if (size[e[i].v] > size[son[x]])
son[x] = e[i].v;
}
}
inline void dfs2(int x,int t){
top[x] = t;dfn[x] = ++ T;
if (!son[x]) return;
dfs2(son[x],t);
for (int i = head[x];i;i = e[i].nxt){
if (e[i].v == fa[x] || e[i].v == son[x])
continue;
dfs2(e[i].v,e[i].v);
}
}
inline int LCA(int a,int b){
while (top[a] != top[b]){
if(d[top[a]] < d[top[b]])
swap(a,b);
a = fa[top[a]];
}
if (d[a] < d[b]) swap(a,b);
return b;
}
inline void ins(int x){
if (stp == 1) {st[++ stp] = x;return;}
int ance = LCA(x,st[stp]);
if (ance == st[stp]) return;
while (stp > 1 && d[ance] < d[st[stp - 1]])
add(st[stp - 1],st[stp]),stp --;
if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
if (!stp || st[stp] != ance) st[++ stp] = ance;
st[++ stp] = x;
}
inline ll DP(int x){
if (!nhead[x]) return mn[x];
ll res = 0;
for (int i = nhead[x];i;i = ne[i].nxt)
res += DP(ne[i].v);
nhead[x] = 0;
return Min(mn[x],res);
}
int main(){
mset(mn,0x3f);scanf("%d",&n);
for (int i = 1;i < n;i ++){
int u,v;ll w;
scanf("%d%d%lld",&u,&v,&w);
ADD(u,v,w);ADD(v,u,w);
}
dfs1(1,0);dfs2(1,1);
scanf("%d",&m);
while (m --){
int k,a[N];ncnt = 1;
scanf("%d",&k);st[stp = 1] = 1;
for (int i = 1;i <= k;i ++)
scanf("%d",&a[i]);
sort(a + 1,a + k + 1,cmp);
for (int i = 1;i <= k;i ++) ins(a[i]);
while (stp) add(st[stp - 1],st[stp]),stp --;
printf("%lld\n",DP(1));
}
return 0;
}
参考资料
[2] 虚树入门 - 自为风月马前卒
[3] 虚树 - OI Wiki