[学习笔记] 第九课:点分治和点分树(动态点分治)
前言
点分治一般是用来解决树上路径统计的问题, 而动态点分治(也称点分树),是用数据结构树上路径信息。
点分治
例题1 给出一颗树,求出所有边权和\(\leq k\)路径数量。
首先显然的\(\Theta (N^2 \log N)\)的枚举算法
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ ) {
if (dis (i, j) <= k) cnt ++ ;
}
}
比较难优化,我们可以换一种考虑方式。
以\(p\)为树根, 则对\(p\)而言, 树上的路径可以分为两种
1.经过\(p\)的路径
2.不经过\(p\)的路径(包含于\(p\)的某个子树中)
第\(2\)种情况我们可以递归(以子树的根再进行计算)
void solve (int u) {
vis[u] = 1; get_dis(u); ans += calc (d, 1);
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (vis[v]) continue;
dis[v] = 1; get_dis (v); ans -= calc (d, -1);//减去子树内重复路径
solve (v);
}
}
这样最劣时间复杂度仍为\(\Theta(N^2 \log N)\) 链既可以卡掉
如果递归到\(T\)层,那么时间复杂度为\(\Theta (TN\log N)\)
如何让\(T\)尽可能的小,我们可以每次选择树的重心作为根节点\(p\),容易证明点分治至多递归\(\log N\)层,时间复杂度也优化到了\(\Theta (N \log ^ 2N)\)
总而言之
点分治的过程为\(4\)步
1.选当前子树的重心\(p\)作为根节点
2.从\(p\)出发进行一次\(dfs\),求出对应信息
3.执行\(solve(p)\)
4.标记\(p\),递归执行\(p\)的每棵子树
例题2 求树上乘积和为\(k\)的路径数
逆元预处理即可
例题3 给定一棵\(n\)个点的树,树上有\(m\)个黑点,求出一条路径,使得这条路径经过的黑点数\(\leq k\),且路径长度最大
用树状数组维护信息(细节:树状数组不能出现\(0\),所以集体向右偏转\(1\))
点分树(动态点分治)
例题4 给定一棵\(N\)个点的树, 每个点都有对应点权\(w_u\),我么有两个操作,要么修改\(x\)点权为\(y\),要么查询距离\(x\)点距离\(\leq K\)的点权和。
首先我们回忆一下点分治的过程,对于当前重心,他管辖的是经过他的路径,那么我们考虑修改一个点,那么有至多有\(\log N\)个重心路径受影响,我们可以用数据结构来维护相关信息,即可在\(\log ^2 N\)的时间解决。
对于每一个点,我们可以开一颗权值树状数组\(/\)权值线段树来维护,由于空间开不下所以常常要用动态开点线段树。维护经过当前点距离为\(s\)的点权之和。
我们可以在处理重心的时候检出一个点分树,及两两重心相连(不用建出来,只要建立上一层的重心即可)。
inline void insert (int rt, int id, int u, int f) {
modify (root[rt][id], 0, n, dis[u], w[u]);
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == f || vis[v]) continue;
dis[v] = dis[u] + 1;
insert (rt, id, v, u);
}
}
inline void solve (int u) {
vis[u] = 1; dep[u] = 0; insert (u, 0, u, 0);
for (int i = head[u]; i; i; i = edge[i].nxt) {
int v = edge[i].to;
if (vis[v]) continue;
S = sz[v]; center = 0; find (v, u); fa[center] = u;
dep[v] = 1; insert (center, 1, v, u);
solve (center);
}
}
我们查询距离\(u\)点距离\(\leq K\)的路径
ans += query (rt[u], 0, n, K);
for (int i = u; fa[i]; i = fa[i]) {
int dis = dist (u, fa[i]);
ans += query (rt[fa[i]], 0, n, K - dis);
}
但这样我们发现有问题,因为有些路径重复算了,我们要减去子树的影响。
ans += query (rt[u][0], 0, n, K);
for (int i = u; fa[i]; i = fa[i]) {
int dis = dist (u, fa[i]);
ans += query (rt[fa[i][0], 0, n, K - dis);
ans -= query (rt[i][1], 0, n, K - dis);
}
return ans;
同理修改也可以得出。
代码 Code