有时做题会想到一些与树深有关的做法,随机数据下是可以过的,但深度稍大就无法过。树深做法的问题在于与深度线性相关。给定的一棵树,其深度是不定的。但使用点分树,可以把原树对应到一颗深度为严格
l
o
g
n
log_n
logn的树上。
建立点分树时,每次选取当前块中的重心,我们把子块的重心作为自己重心的儿子,就形成一颗点分树。可以证明其深度不超过
l
o
g
n
log_n
logn。
这样,一些严重依赖树形态的算法,如树形DP,或从全局角度统计树中的某些量的问题,都可以轻易用点分树动态维护。
构建点分树如下。注意到每次重新getgra是在一个子树内完成的,因此不用考虑影响统计其他子树。
int gra, gras;
int cn; // 当前块大小
int siz[maxn], vis[maxn];
void getgra(int u, int pa) { //找当前联通块的重心
siz[u] = 1; int maxs = 0;
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i].to;
if(v == pa || vis[v]) continue;
getgra(v, u);
siz[u] += siz[v];
maxs = max(maxs, siz[v]);
}
maxs = max(maxs, cn-siz[u]);
if(gras == -1 || maxs < gras) {
gras = maxs;
gra = u;
}
}
struct dt_node { // 点分树节点
int f;
//记录你要维护的值
}dt[maxn];
void build(int u, int pa) {
dt[u].f = pa;
vis[u] = 1; // 已选作重心
int ccn = cn; // ccn = 自己现在块大小(不是子树的)
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i].to;
if(vis[v]) continue;
gras = -1;
if(siz[v] > siz[u]) cn = ccn - siz[u]; else cn = siz[v]; // 父亲子树与儿子子树计算cn的方式不同
getgra(v, u);
build(gra, u);
}
}
int main() {
//...
cn = n; gras = -1;
getgra(0, -1);
build(gra, -1);
}
点分树的应用很多。维护每个点分树节点也有多种类型,如维护子树和对父亲贡献两个值,或维护一个堆记录子节点到它的距离(bzoj1095)等。总之是统计子树内的信息,然后每次更新或询问时就从操作节点在点分树上往父亲走,其实与树深算法如出一辙,只是这是正解。
点分树也与很多其他树上操作技巧结合。可以参考其他树上操作技巧或树上操作技巧总结。
另:标题中“实”指原树,“虚”指与原树等效的树,如LCT、点分树、dfs序线段树(扩展为路径剖分树/欧拉旅游树)等。
upd on 2019.2.1
点分树支持的操作一般是单点修改和单点查询,常带有距离限制。修改即对该点在点分树上的结点到点分树根的链修改。可考虑对每个点分树节点开一颗线段树表示对应深度的答案,再记录每个点分树节点对应的原树中的子树以点分树节点对应的点为根的深度即可。注意每个点分树节点对应的原树中子树的大小和是
O
(
n
log
n
)
O(n\log n)
O(nlogn)的。也有不少题目要考虑消去自己对点分父亲的影响,因此还要记录对点分父亲的贡献。