虚树学习笔记
虚树
基本概念:
虚树是指在原树上选择若干点组成的树,它在原树的基础上做了一些简化,但是保留必要的信息,从而使得计算更加高效。
虚树主要用于树形DP中,能够减少顶点数,降低时间复杂度。
例题:
[SDOI2011] 消耗战
题目大意
给出一棵树,n个顶点。每条边有边权。有m次询问,每次询问给出k个询问点,问使得这
k个点均不与1号点(根节点)相连的最小代价。
分析:
这道题可以用树形dp来做:
显然:我们设\(dp[i]\)表示以\(i\)为根的子树内满足题意的最小代价,\(mind[i]\)表示点\(i\)到根节点的路径中最小边权是多少
1、点\(i\)是询问点:\(dp_i = mind_i\)
2、点\(i\)不是询问点:\(dp_i = min (mind_i , \sum dp_j(j是i的儿子子树j里面有询问点))\)
分析一下时间复杂度,发现是 O(n*m) 的,好像过不了
此时我们就要想一想 优化
因为我们每次其实只会考虑那些询问点,而询问点的数量满足 \(k<=500005\) ,所以我们花费了大多数时间在跑那些没有意义的节点。
所以我们需要 重建一棵树,使得树上所有节点都是有意义的,这就是虚树 即只包含所有的询问点和他们的\(lca\)
虚树维护
首先要给原树做一次\(dfs\),打上时间戳\(dfn\)
将要查询的点按照 \(dfn\) 排序,
然后搞一个栈,里面存的是一条 \(1\) 号点到当前点的链。
栈中的点之间暂未连边,最后退栈的时候再连边。
先给1号节点入栈,\(stk[++top] = 1\)
然后考虑当前待加入的节点\(p(p是询问点)\)
1、如果\(stk[top] == lca (p , stk[top])\) , 那么一定满足条件,直接进栈,\(stk[++top] = p\)。否则,\(p\) 和\(stk[top]\) 一定是不同子树里面的。
2、然后\(while(dfn[lca(stk[top] , p)] <= dfn[stk[top - 1]])\) 则连边并退栈 \(add(stk[top - 1] , stk[top]),top --\)。
3、最后如果\(lca(stk[top] , p) \neq stk[top]\) ,那么连边 \((lca(stk[top] , p) , stk[top])\),将 \(lca\) 入栈 \(stk[top] = Lca(stk[top] , p)\)。将 \(p\) 入栈,然后退出。
把所有关键点处理完之后要把栈中的节点连边并退栈
\(while(top)\) 连边 \(add (stk[top - 1] , stk[top]) , top --\)
可能下一题的虚树代码更好看一点
code
#include<bits/stdc++.h>
#define fu(x , y , z) for (int x = y ; x <= z ; x ++)
#define LL long long
#define fd(x , y , z) for (int x = y ; x >= z ; x --)
using namespace std;
const int N = 250005 , M = 5e5 + 5;
struct E {
int to , nt;
LL w;
} e[N << 1];
int p1[N] , p , dep[N] , top[N] , sz[N] , son[N] , hd[N] , cnt , fa[N] , tp , stk[N] , flg[N] , m , h[M] , k , u , v , n , h1;
LL dp[N] , mind[N] , wi;
void add (int x , int y , int z) { e[++cnt].to = y , e[cnt].nt = hd[x] , e[cnt].w = z , hd[x] = cnt; }
inline void dfs1 (int x) {
int y , maxs = 0;
sz[x] = 1;
for (int i = hd[x] ; i ; i = e[i].nt) {
y = e[i].to;
if (y == fa[x])
continue;
dep[y] = dep[x] + 1;
fa[y] = x;
mind[y] = min (mind[x] , e[i].w);
dfs1 (y);
sz[x] += sz[y];
if (sz[y] > maxs) {
maxs = sz[y];
son[x] = y;
}
}
}
inline void dfs2 (int x) {
p1[x] = ++p;
int y;
if (son[x]) {
top[son[x]] = top[x];
dfs2 (son[x]);
}
else
return;
for (int i = hd[x] ; i ; i = e[i].nt) {
y = e[i].to;
if (y == fa[x] || y == son[x])
continue;
top[y] = y;
dfs2 (y);
}
}
int Lca (int x , int y) {
while (top[x] != top[y]) {
if (dep[fa[top[x]]] > dep[fa[top[y]]])
swap(x , y);
y = fa[top[y]];
}
return dep[x] < dep[y] ? x : y;
}
inline void Insert (int x) {
int y = stk[tp];
int lca = Lca (x , y);
while (dep[y] > dep[lca] && tp) {
if (dep[stk[tp - 1]] < dep[lca]) {
add (lca , y , 0);
}
else
add (stk[tp - 1] , stk[tp] , 0);
tp--;
y = stk[tp];
}
if (y == lca) {
stk[++tp] = x;
}
else {
stk[++tp] = lca;
stk[++tp] = x;
}
}
inline void dfs3 (int x) {
int y;
long long sum = 0;
for (int i = hd[x] ; i ; i = e[i].nt) {
y = e[i].to;
if (y == fa[x])
continue;
dfs3 (y);
sum += dp[y];
}
if (flg[x]) {
dp[x] = mind[x];
}
else {
dp[x] = min (mind[x] , sum);
}
hd[x] = 0;
}
bool cmp (int x , int y) { return p1[x] < p1[y]; }
void solve () {
int m;
scanf ("%d" , &m);
fu (T , 1 , m) {
scanf ("%d" , &k);
fu (i , 1 , k) scanf ("%d" , &h[i]);
tp = 1;
stk[1] = 1;
cnt = 0;
sort (h + 1 , h + k + 1 , cmp);
fu (i , 1 ,k) {
Insert (h[i]);
flg[h[i]] = 1;
}
for (int i = tp ; i > 1 ; i --)
add (stk[i-1] , stk[i] , 0);
dfs3 (1);
fu (i , 1 , k) flg[h[i]] = 0;
printf ("%lld\n" , dp[1]);
}
}
int main() {
int u , v;
LL w;
dep[1] = 1;
scanf ("%d" , &n);
fu (i , 1 , n) mind[i] = 1e18;
fu (i , 1 , n - 1) {
scanf ("%d%d%d" , &u , &v , &w);
add (u , v , w) , add (v , u , w);
}
dfs1 (1);
dfs2 (1);
cnt = 0;
memset (hd , 0 , sizeof (hd));
solve ();
return 0;
}
CF613D Kingdom and its Cities
题目大意
给出一棵树,树上有 \(k\) 个关键节点,你可以删掉树上的非关键点,请问使得所有的关键点互不连通最少需要删除的非关键点数
有 \(m\) 组询问,每次更新关键节点。
\(1 \le n \le 10^5 , 1 \le m \le 10 ^5 , \sum k\le10^5\)
分析
这里主要讲一下 \(dp\)
对于每个点维护
\(g[x]\) 为以 \(i\) 为根节点的子树中有没有关键点与 \(x\) 连通 如果x为关键点则 g[x] = 1
\(f[x]\) 为以 \(x\) 为根的答案
首先 \(f[x] = \sum_{v = son(x)} f[v]\)
然后分类讨论。
- 如果 \(x\) 为关键点,那么 \(f[x] += \sum_{v = son(x)}g[v]\) ,也就是把所有有关键点的后代全部断掉。
- 乳沟 \(x\) 不是关键点,如果它有多于一个的有关键点儿子,就把 \(x\) 断掉 \(f[x] ++ , g[x] = 0\) ,否则就留给祖先处理(可能跟其他的一起断掉更优)\(g[x] = 1\)
然后还是一样的虚树处理方式。
code
#include <bits/stdc++.h>
#define fu(x , y , z) for(int x = y ; x <= z ; x ++)
#define fd(x , y , z) for(int x = y ; x >= z ; x --)
using namespace std;
const int N = 1e6 + 5 , K = 20;
int k , fa[N][K + 5] , dep[N] , n , m , rt , flg[N] , dfn[N] , pos , p[N] , f[N] , f1[N] , sz[N];
int stk[N] , top;
vector<int> g[N];
int read () {
int val = 0;
char ch = getchar ();
while (ch < '0' || ch > '9') ch = getchar ();
while (ch >= '0' && ch <= '9') {
val = val * 10 + (ch - '0');
ch = getchar ();
}
return val;
}
void add (int x , int y) { g[x].push_back(y); }
void pre (int x) {
int y;
dfn[x] = ++pos;
for (auto i : g[x]) {
if (i == fa[x][0]) continue;
fa[i][0] = x;
dep[i] = dep[x] + 1;
pre (i);
}
}
int Lca (int x , int y) {
if (dep[x] < dep[y]) swap (x , y);
fd (i , K , 0)
if (dep[fa[x][i]] >= dep[y]) x = fa[x][i];
if (x == y) return x;
fd (i , K , 0)
if (fa[x][i] != fa[y][i]) x = fa[x][i] , y = fa[y][i];
return fa[x][0];
}
bool cmp (int x , int y) { return dfn[x] < dfn[y]; }
void build () {
top = 1;
stk[1] = 1;
g[1].clear();
fu (i , 1 , k) {
if (p[i] == 1) continue;
int lca = Lca (p[i] , stk[top]);
if (lca != stk[top]) {
while (dfn[stk[top - 1]] > dfn[lca]) {
add (stk[top - 1] , stk[top]);
top --;
}
if (lca != stk[top - 1]) {
g[lca].clear();
add (lca , stk[top]);
stk[top] = lca;
}
else add (lca , stk[top --]);
}
g[p[i]].clear() , stk[++top] = p[i];
}
fu (i , 1 , top - 1) add (stk[i] , stk[i + 1]);
}
void fans (int x) {
f[x] = sz[x] = 0;
for (auto i : g[x]) {
fans (i);
f[x] += f[i] , sz[x] += sz[i];
}
if (flg[x]) f[x] += sz[x] , sz[x] = 1;
else if (sz[x] > 1) f[x] ++ , sz[x] = 0;
}
int main () {
int u , v;
n = read ();
fu (i , 1 , n - 1) {
u = read () , v = read ();
add (u , v) , add (v , u);
}
dep[1] = 1;
pre (1);
fu (i , 1 , K)
fu (j , 1 , n) fa[j][i] = fa[fa[j][i - 1]][i - 1];
m = read ();
int x , bz;
while (m --) {
k = read ();
fu (i , 1 , k) {
p[i] = read ();
flg[p[i]] = 1;
}
bz = 0;
fu (i , 1 , k) {
if (flg[fa[p[i]][0]]) {
bz = 1;
printf ("-1\n");
break;
}
}
if (bz == 1) {
fu (i , 1 , k) flg[p[i]] = 0;
continue;
}
sort (p + 1 , p + k + 1 , cmp);
build ();
fans (1);
printf ("%d\n" , f[1]);
fu (i , 1 , k) flg[p[i]] = 0;
}
return 0;
}
后记
2024.5.11做了虚树的专题,发现了多处错误,已改正,也添加了一些部分。