点分树小记
Preface
点分树和点分治好像一直是 OI 中的热门考点,出现频率并不算太低,而且为了迎合自己的学习清单计划,这里大概就是说每学一个就会写一篇博客来记录一下自己真的学过这玩意。
Main
点分树是在点分治的基础之上,通过数据结构去维护树内联通块的信息,维护深度相关的信息。
分治重心有良好的性质,那就是分治之后一个子树仍然是一个连通块,那么我们就可以继续分治下去。
根据重心的性质,我们最终的点分树最多有 \(\lg n\) 层,每一层都记录相关子树的深度信息,从而达到维护树上点与点之间距离的效果。
namespace PDT {
// struct BitTree {
// int n;
// vector<int> va;
// inline void bld(int s) {va.resize((n = s + 1) + 1);}
// inline int lbt(int x) {return x & -x;}
// inline void add(int x, int k) {
// for(x = min(x + 1, n); x <= n; x += lbt(x))
// va[x] += k;
// return ;
// }
// inline int chk(int x, int k, int ret = 0) {
// for(x = min(x + 1, n); x >= 1; x -= lbt(x))
// ret += va[x];
// return ret;
// }
// } F[N], G[N];
int mx[N], sz[N], dep[N], d[N][20], fa[N];
inline void chk(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0) {
for(int v : e[x]) if(!dep[v] && v != pre)
chk(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), chk(t, x), t;
}
void sol(int x, int D, int &dx, int &df, int pre = 0) {
cmax(dx, d[x][D] = d[pre][D] + 1);
camx(df, d[x][D - 1]), sz[x] = 1 ;
for(int v : e[x]) if(!dep[v] && v != pre)
sol(v, D, dx, df, x), sz[x] += sz[v];
return ;
}
void dfs(int x, int D, int w, int pre = 0, int dx = 0, int df = 0) {
fa[x = frt(x, w)] = pre, sol(x, D, dx, df);
// F[x].bld(dx), G[x].bld(df);
for(int v : e[x]) if(!dep[v])
dfs(v, D + 1, sz[v], x);
return ;
}
void init() {
for(int i = 0; i <= 19; i++)
d[0][i] = -1;
return mx[0] = n + 10, dfs(1, 1, n);
}
}
这是一个实现的较为优秀的点分树建树过程,建树的时间复杂度和空间复杂度都是 \(O(n\lg n)\) 的。
建出点分树之后的内容就是八仙过海各显神通了。
从简单的模板题入手。
这道题要求我们修改某一个点的权值,然后求一个点 \(k\) 距离以内的所有点的权值和。
维护每一个分治重心的子树下所有深度的点的信息,然后再维护其分治重心父亲在当前子树下所有深度的点的信息,每一次查询就可以通过查询其所有的祖先节点来进行操作了,时间复杂度是 \(O(\lg^2 n)\) 的。
代码:
namespace PDT {
struct BitTree {
int n;
vector<int> va;
inline int lbt(int x) {return x & -x;}
inline void bld(int s) {va.resize((n = s + 1) + 1);}
inline void add(int x, int k) {
x++;
while(x && x <= n) va[x] += k, x += lbt(x);
return ;
}
inline int ask(int x, int ret = 0) {
cmin(++x, n);
while(x >= 1) ret += va[x], x -= lbt(x);
return ret;
}
} F[N], G[N];
int mx[N], sz[N], fa[N], d[N][20], dep[N];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0) {
mx[x] = 0, sz[x] = 1;
for(int v : e[x]) if(!dep[v] && v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
cmax(mx[x], al - sz[x]), check(t, x);
return t;
}
int dx = 0, dw = 0;
void sol(int x, int D, int pre = 0) {
cmax(dx, d[x][D] = d[pre][D] + 1);
cmax(dw, d[x][D - 1]), sz[x] = 1 ;
for(int v : e[x]) if(!dep[v] && v != pre)
sol(v, D, x), sz[x] += sz[v];
return ;
}
void dfs(int x, int D, int w, int pre = 0) {
fa[x = frt(x, w)] = pre, sol(x, dep[x] = D);
// dbg(x);
F[x].bld(dx), G[x].bld(dw), dx = dw = 0;
for(int v : e[x]) if(!dep[v])
dfs(v, D + 1, sz[v], x);
}
void upd(int x, int y) {
for(int t = x, *b = d[x] + dep[x]; t; t = fa[t])
F[t].add(*b, y), G[t].add(*--b, y);
return ;
}
int qry(int x, int k) {
int ret = F[x].ask(k);
// dbg(ret);
for(int t = x, *b = d[x] + dep[x] - 1; fa[t];--b, t = fa[t])
ret += F[fa[t]].ask(k - *b) - G[t].ask(k - *b);
// dbg(ret);
// debug(" ================= \n");
return ret;
}
void init() {
for(int i = 0; i <= 19; i++)
d[0][i] = -1;
mx[0] = n + 10, dfs(1, 1, n);
return ;
}
}
练习题:烁烁的游戏
不过是将查询块单点修变成修改块单点查,同样的套路,在查询处稍作改变即可。
void mdf(int x, int k, int w) {
F[x].add(k, w);
for(int t = x, *b = d[x] + dep[x] - 1; fa[t];--b, t = fa[t]) {
F[fa[t]].add(k - *b, w), G[t].add(k - *b, w);
}
}
int qry(int x) {
int ret = F[x].ask(0);
for(int t = x, *b = d[x] + dep[x] - 1; fa[t];--b, t = fa[t]) {
ret += F[fa[t]].ask(*b) - G[t].ask(*b);
}
return ret;
}
练习题:atm 的树
我们可以二分一个答案,然后判断这个答案内的距离有多少个,维护距离可以用平衡树维护。
namespace PDT {
int cnt = 0;
__gnu_pbds::tree<pair<int, int>, __gnu_pbds::null_type, less<pair<int, int> >,
__gnu_pbds::rb_tree_tag,
__gnu_pbds::tree_order_statistics_node_update> F[N], G[N];
int dep[N], fa[N], d[N][20], mx[N], sz[N];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0, int v = 0) {
sz[x] = 1, mx[x] = 0;
for(auto ts : e[x]) if(!dep[v = ts.first] && v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), check(t, x), t;
}
void sol(int x, int D, int tp, int dis = 0, int pre = 0, int v = 0) {
F[tp].insert(make_pair(d[x][D] = dis, cnt++));
G[tp].insert(make_pair(d[x][D - 1], cnt++)), sz[x] = 1;
for(auto ts : e[x]) if(!dep[v = ts.first] && v != pre)
sol(v, D, tp, dis + ts.second, x), sz[x] += sz[v];
return ;
}
void dfs(int x, int D, int w, int pre = 0, int v = 0) {
fa[x = frt(x, w)] = pre, sol(x, dep[x] = D, x);
for(auto ts : e[x]) if(!dep[v = ts.first])
dfs(v, D + 1, sz[v], x);
return ;
}
inline void init() {
return mx[0] = n + 10, dfs(1, 1, n);
}
inline int ask(int x, int y) {
int ret = F[x].order_of_key(make_pair(y, INF));
for(int t = x, *b = d[x] + dep[x] - 1; fa[t];b--, t = fa[t])
ret += F[fa[t]].order_of_key(make_pair(y - *b, INF)) - G[t].order_of_key(make_pair(y - *b, INF));
// dbg(ret);
return ret;
}
}
inline void solve() {
n = rd, k = rd;
for(int i = 1; i < n; i++) {
int u = rd, v = rd, w = rd;
e[u].emplace_back(v, w);
e[v].emplace_back(u, w);
}
PDT::init();
for(int i = 1; i <= n; i++) {
int l = 0, r = 1e6, ans = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(PDT::ask(i, mid) <= k) l = mid + 1, ans = mid;
else r = mid - 1;
}
cout << ans + 1 << '\n';
}
return ;
}
有时候码风有一定差异请见谅。
不过我的写法好像有一点小问题,是会被叉掉的,眼尖的同学可以随便叉一下,我过了就懒得改了。
例题: [HNOI2015]开店
实际上这一题不用维护深度信息,我们需要维护的是点权大小,所以我们以点权为关键字排序,得到一个递增的点权大小信息,然后做一个后缀和即可。
为什么是后缀和?因为前缀和在查询的时候会有一点麻烦,后缀和可以减去一下边界的特判。
然后按照题目要求随便做做就可以了。
namespace PDT {
vector< pair<int, ll> > F[N], G[N];
int mx[N], sz[N], fa[N], dep[N];
ll d[N][20];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0, int v = 0) {
mx[x] = 0, sz[x] = 1;
for(auto st : e[x]) if(!dep[v = st.first] && v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), check(t, x), t;
}
void sol(int x, int D, int tp, ll dis = 0, int pre = 0, int v = 0) {
F[tp].push_back(make_pair(a[x], d[x][D] = dis));
G[tp].push_back(make_pair(a[x], d[x][D - 1])), sz[x] = 1;
for(auto st : e[x]) if(!dep[v = st.first] && v != pre)
sol(v, D, tp, dis + st.second, x), sz[x] += sz[v];
return ;
}
void dfs(int x, int D, int w, int pre = 0, int v = 0) {
fa[x = frt(x, w)] = pre, sol(x, dep[x] = D, x);
sort(F[x].begin(), F[x].end());
sort(G[x].begin(), G[x].end());
for(int i = F[x].size() - 2; ~i; i--)
F[x][i].second += F[x][i + 1].second;
for(int i = G[x].size() - 2; ~i; i--)
G[x][i].second += G[x][i + 1].second;
F[x].emplace_back(2e9, 0), G[x].emplace_back(2e9, 0);
for(auto st : e[x]) if(!dep[v = st.first])
dfs(v, D + 1, sz[v], x);
return ;
}
void init() {
return mx[0] = n + 10, dfs(1, 1, n);
}
inline pair<int, ll> qryF(int x, int l, int r) {
l = lower_bound(F[x].begin(), F[x].end(), make_pair(l, 0ll)) - F[x].begin();
r = upper_bound(F[x].begin(), F[x].end(), make_pair(r, INF)) - F[x].begin();
return make_pair(r - l, F[x][l].second - F[x][r].second);
}
inline pair<int, ll> qryG(int x, int l, int r) {
l = lower_bound(G[x].begin(), G[x].end(), make_pair(l, 0ll)) - G[x].begin();
r = upper_bound(G[x].begin(), G[x].end(), make_pair(r, INF)) - G[x].begin();
return make_pair(r - l, G[x][l].second - G[x][r].second);
}
ll ask(int x, int L, int R) {
ll ret = qryF(x, L, R).second;
for(ll t = x, *b = d[x] + dep[x] - 1; fa[t]; b--, t = fa[t]) {
auto A = qryF(fa[t], L, R);
auto B = qryG(t, L, R);
ll ans1 = *b * (A.first - B.first);
ll ans2 = A.second - B.second;
ret += ans1 + ans2;
}
return ret;
}
}
练习题:Book of Evil
看完上面的题面做这题反而有点小儿科了,有两种做法,比较无脑的点分树做法就是直接建点分树然后判断距离 \(d\) 以内是否有 \(m\) 个点既可。
代码不贴了……
本题要求带权重心,不难发现数据范围中的重要性质,就是一个点的度数最多为 \(20\)。
所以我们考虑暴力转移,每一个点只会枚举 \(20\) 次转移,整体的查询时 \(O(dep)\) 的,所以我们用从重心枚举的思想,于是就有 \(O(dep) = O(\lg n)\) 的复杂度,这是我们可以接受的。
接下来就是要快速求出一个点的权值,我们可以用点分树轻松维护这个问题。
时间复杂度 \(O(n\lg^2 n)\)。
namespace PDT {
ll F[2][N], G[2][N];
int mx[N], sz[N], dep[N], d[N][20], fa[N][20];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0, int v = 0) {
mx[x] = 0, sz[x] = 1;
for(auto st : e[x]) if(!dep[v = st.first] && v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), check(t, x), t;
}
void sol(int x, int D, int tp, int dis = 0, int pre = 0, int v = 0) {
fa[x][D] = tp, d[x][D] = dis, sz[x] = 1;
for(auto st : e[x]) if(!dep[v = st.first] && v != pre)
sol(v, D, tp, dis + st.second, x), sz[x] += sz[v];
return ;
}
void mkt(int x, int D, int w, int pre = 0, int v = 0) {
fa[x = frt(x, w)][0] = pre, sol(x, dep[x] = D, x);
for(auto st : e[x]) if(!dep[v = st.first])
mkt(v, D + 1, sz[v], x);
return ;
}
void init() {
return mx[0] = n + 10, mkt(1, 1, n);
}
void upd(int x, ll k) {
F[0][x] += k;
for(int t = x, *b = d[x] + dep[x] - 1; fa[t][0]; b--, t = fa[t][0]) {
F[0][fa[t][0]] += k, F[1][fa[t][0]] += k * (*b);
G[0][t] += k, G[1][t] += k * (*b);
}
return ;
}
ll ask(int x) {
ll ret = F[1][x];
for(int t = x, *b = d[x] + dep[x] - 1; fa[t][0]; b--, t = fa[t][0]) {
ret += (F[0][fa[t][0]] - G[0][t]) * (*b);
ret += F[1][fa[t][0]] - G[1][t];
}
return ret;
}
inline ll fnd(int x, int D, int v = 0) {
ll tem = ask(x);
for(auto st : e[x])
if(ask(v = st.first) < tem) return fnd(fa[v][D + 1], D + 1);
return tem;
}
}
记得开 long long
。
练习题:小清新数据结构题
假设此时 \(S_i\) 为以 \(x\) 为根的时候的子树大小,我们需要求的就是 \(\sum\limits_{i=1}^n S_i^2\)。
本题之后,这就是一个套路式子了,我们可以很容易求出 \(\sum\limits_{i=1}^nS_i\) 的值,不难发现就是 \(\sum\limits_{i=1}^n a_i(dis(i,x)+1)\)。
拆开之后可以得到:
\(sum\) 指的是 \(\sum\limits_{i=1}^n a_i\)。后面那个式子可以像幻想乡战略游戏一样用点分树维护。
接着我们需要证明,无论根是哪个点, \(\sum\limits_{i=1}^nS_i(sum-S_i)\) 都是一定的。
实际上这就是枚举每一条边,然后将左右边的子树权值和乘起来,我们打开乘法之后可以得到:\(\sum\limits_{1\leq i < j\leq n}dis(i,j) a_ia_j\)。
然后我们就可以发现这玩意确实和根是啥无关,所以我们得到答案就是:
前者中 \(\sum\limits_{i=1}^nS_i\) 已经在上面有证明了,可以 \(O(\lg n)\) 完成修改和查询,后者可以考虑处理 \(\sum\limits_{1\leq i < j \leq n}dis(i,j)a_ia_j\)。
时间复杂度 \(O(n\lg n)\),然后就被树剖 \(O(n\lg^2n)\) 给爆锤了。
namespace PDT {
ll W = 0, sum = 0;
ll F[2][N], G[2][N];
int mx[N], sz[N], fa[N], dep[N], d[N][20];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0) {
mx[x] = 0, sz[x] = 1;
for(int v : e[x]) if(!dep[v] && v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), check(t, x), t;
}
void sol(int x, int D, int pre = 0) {
sz[x] = 1, d[x][D] = d[pre][D] + 1;
for(int v : e[x]) if(!dep[v] && v != pre)
sol(v, D, x), sz[x] += sz[v];
return ;
}
void dfs(int x, int D, int w, int pre = 0) {
fa[x = frt(x, w)] = pre, sol(x, dep[x] = D);
for(int v : e[x]) if(!dep[v])
dfs(v, D + 1, sz[v], x);
return ;
}
void init() {
for(int i = 0; i <= 19; i++)
d[0][i] = -1;
return mx[0] = n + 10, dfs(1, 1, n);
}
ll calc(int x) {
ll ret = F[1][x];
for(int t = x, *b = d[x] + dep[x] - 1; fa[t]; b--, t = fa[t]) {
ret += F[1][fa[t]] - G[1][t];
ret += (F[0][fa[t]] - G[0][t]) * (*b);
}
return ret;
}
void upd(int x, ll k) {
W += k * calc(x), sum += k;
F[0][x] += k;
for(int t = x, *b = d[x] + dep[x] - 1; fa[t]; b--, t = fa[t]) {
F[0][fa[t]] += k, F[1][fa[t]] += k * (*b);
G[0][t] += k, G[1][t] += k * (*b);
}
return ;
}
ll ask(int x) {
return sum * (sum + calc(x)) - W;
}
}
例题:[ZJOI2007]捉迷藏。
点分树建好之后,一个比较显然的思路就是,以当前分治点为根,查所有子树中白色点离当前点的最大距离,以及次大距离,但是两个距离必须来自不同子树,然后查所有分治点的最大值就可以了。
具体而言,我们需要维护三个东西。
第一个是子树中白色点离父亲的最大距离,第二个是当前点的所有子树中白色点离自己的最大距离,第三个就是所有点的答案。
实际上思路清晰之后就非常容易写出代码了,三个东西都可以用堆来维护,然后随便做一做就可以了。
时间复杂度 \(O(n\lg^2n)\)
namespace PDT {
struct node {
priority_queue<int> a, b;
inline void push(int x) {a.push(x);}
inline void delt(int x) {b.push(x);}
inline int top() {
while(!b.empty() && a.top() == b.top())
a.pop(), b.pop();
return a.top();
}
inline int size() {return a.size() - b.size();}
inline void pop() {
while(!b.empty() && a.top() == b.top())
a.pop(), b.pop();
a.pop();
}
inline int sec() {
int x = top();
pop();
int y = top();
return push(x), y;
}
} ans, F[N], G[N];
int mx[N], sz[N], fa[N], dep[N], d[N][20];
inline void check(int &x, int y) {if(mx[x] > mx[y]) x = y;}
int frt(int x, int al, int pre = 0, int t = 0, int v = 0) {
mx[x] = 0, sz[x] = 1;
for(auto st : e[x]) if(!dep[v = st.first] and v != pre)
check(t, frt(v, al, x)), sz[x] += sz[v], cmax(mx[x], sz[v]);
return cmax(mx[x], al - sz[x]), check(t, x), t;
}
void sol(int x, int D, int tp, int dis = 0, int pre = 0, int v = 0) {
d[x][D] = dis, sz[x] = 1;
if(D - 1) F[tp].push(d[x][D - 1]);
for(auto st : e[x]) if(!dep[v = st.first] && v != pre)
sol(v, D, tp, dis + st.second, x), sz[x] += sz[v];
return ;
}
inline void add(int x) {
if(G[x].size() >= 2) ans.push(G[x].top() + G[x].sec());
}
inline void del(int x) {
if(G[x].size() >= 2) ans.delt(G[x].top() + G[x].sec());
}
void dfs(int x, int D, int w, int pre = 0, int v = 0) {
fa[x = frt(x, w)] = pre, sol(x, dep[x] = D, x);
if(fa[x]) G[fa[x]].push(F[x].top());
for(auto st : e[x]) if(!dep[v = st.first])
dfs(v, D + 1, sz[v], x);
G[x].push(0), add(x);
return ;
}
inline void init() {
for(int i = 0; i <= 19; i++)
d[0][i] = -1;
return mx[0] = n + 10, dfs(1, 1, n);
}
inline void ON(int x) {
del(x), G[x].delt(0), add(x);
for(int t = x, *b = d[x] + dep[x] - 1; fa[t]; b--, t = fa[t]) {
del(fa[t]), G[fa[t]].delt(F[t].top()), F[t].delt(*b);
if(F[t].size()) G[fa[t]].push(F[t].top());
add(fa[t]);
}
}
inline void OFF(int x) {
del(x), G[x].push(0), add(x);
for(int t = x, *b = d[x] + dep[x] - 1; fa[t]; b--, t = fa[t]) {
del(fa[t]);
if(F[t].size()) G[fa[t]].delt(F[t].top());
F[t].push(*b), G[fa[t]].push(F[t].top()), add(fa[t]);
}
return ;
}
inline int ask() {return ans.top();}
}