点分树就是将每一次的重心连起来, 形成一个深度不超过log的树
相当于将点分治的过程静态到一颗树上
实现如下, fa记录点分树上的父亲
void Divide(int u, int f){
fa[u] = f; vis[u] = 1; int sum = Siz;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(vis[t]) continue;
if(siz[t] > siz[u]) Siz = sum - siz[u];
else Siz = siz[t];
rt = 0; getrt(t, 0); Divide(rt, u);
}
}
然后我们可以在点分树上每个点维护一种数据结构, 来完成题目中的修改要求
例1: BZOJ3730 震波
建出点分树后可以每个点维护一个树状数组, 表示 u 子树中到 u 距离为 1/2/3 ... 的点的权值和
那么 对于u的子树, <= k 的树状数组查一下就可以了。
然后考虑经过 u 在点分树上的祖先 fa, 那么是不是可以在 fa的树状数组中查 <= k - dis(fa, u) 的和呢
容斥一下将 u 子树中的贡献减掉, 于是再维护一个树状数组表示 u 子树中到 fa 距离为 1/2/3... 的点权和
这样最多查 logn次, 因为每次跳fa最多log层, 每次要log查询, 所以复杂度是 log^2
树状数组可以用 vector 动态开点, 这样空间是 nlog的
#include<bits/stdc++.h>
#define N 500050
using namespace std;
int first[N], nxt[N], to[N], tot;
void add(int x, int y){
nxt[++tot] = first[x], first[x] = tot, to[tot] = y;
}
int n, m, val[N];
struct vec{
vector<int> v; int s;
void init(){ v.assign(s+10, 0);}
void update(int x, int val){
x++; for(;x<=s;x+=x&-x) v[x]+=val;
}
int Ask(int x){
x++; x = min(x, s); int res = 0;
for(;x>=1;x-=x&-x) res += v[x]; return res;
}
}f1[N], f2[N];
int st[N][22], sign, id[N], dis[N], lg[N];
void dfs(int u, int f){
st[++sign][0] = dis[u]; id[u] = sign;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f) continue;
dis[t] = dis[u] + 1; dfs(t, u);
st[++sign][0] = dis[u];
}
}
int siz[N], Maxson[N], rt, Siz, vis[N];
void getrt(int u, int f){
siz[u] = 1; Maxson[u] = 0;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f || vis[t]) continue;
getrt(t, u); siz[u] += siz[t];
Maxson[u] = max(Maxson[u], siz[t]);
} Maxson[u] = max(Maxson[u], Siz - siz[u]);
if(Maxson[u] < Maxson[rt]) rt = u;
}
int fa[N], dep[N], d[N], res;
int dist(int u, int v){
int x = id[u], y = id[v];
if(x > y) swap(x, y);
int t = lg[y - x + 1];
return dis[u] + dis[v] - 2 * min(st[x][t], st[y-(1<<t)+1][t]);
}
void getdis(int u, int f){
d[dep[u]] += val[u]; res = max(res, dep[u]);
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f || vis[t]) continue;
dep[t] = dep[u] + 1; getdis(t, u);
}
}
void calc(vec &x){
x.s = res + 1; x.init();
for(int i=0; i<=res; i++){
x.update(i, d[i]); d[i] = 0;
} res = 0;
}
void Divide(int u, int f){
fa[u] = f; vis[u] = 1; dep[u] = 0; getdis(u, 0);
calc(f1[u]); int sum = Siz;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(vis[t]) continue;
if(siz[t] > siz[u]) Siz = sum - siz[u];
else Siz = siz[t];
rt = 0; getrt(t, 0); dep[t] = 1; getdis(t, 0);
calc(f2[rt]); dep[t] = 0; Divide(rt, u);
}
}
int ans;
int Qu(int x, int k){
int res = 0;
for(int i=x; i; i = fa[i]){
res += f1[i].Ask(k - dist(i, x));
}
for(int i=x; fa[i]; i = fa[i]){
res -= f2[i].Ask(k - dist(fa[i], x));
} return res;
}
void Modify(int x, int k){
for(int i=x; i; i = fa[i]){
f1[i].update(dist(x, i), k);
}
for(int i=x; fa[i]; i = fa[i]){
f2[i].update(dist(fa[i], x), k);
}
}
int main(){
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++) scanf("%d", &val[i]);
for(int i=1; i<n; i++){
int x, y; scanf("%d%d", &x, &y);
add(x, y); add(y, x);
} dfs(1, 0);
for(int i=2; i<=sign; i++) lg[i] = lg[i>>1] + 1;
for(int i=1; (1<<i) <= sign; i++)
for(int j=1; j+(1<<i)-1 <= sign; j++)
st[j][i] = min(st[j][i-1], st[j+(1<<(i-1))][i-1]);
Siz = n; Maxson[0] = 1e9;
getrt(1, 0);
Divide(rt, 0);
while(m--){
int op, x, y; scanf("%d%d%d", &op, &x, &y); x ^= ans; y ^= ans;
if(op == 0) ans = Qu(x, y), printf("%d\n", ans);
if(op == 1) Modify(x, y - val[x]), val[x] = y;
} return 0;
}
我们经过反思后发现 , 如果每次暴力点分治会把n个重心全部查一遍,但与当前点u有关的只有log个
我们通过点分树将这log个找出来, 并在这log个中查询,维护, 从而达到了可观的复杂度
我们考虑假设当前最优点是x,如果修改了一个点权,就会想某个儿子偏移
我们可以枚举这个儿子(因为度数<=20), 然后计算在这个儿子的答案,如果比当前小就往儿子走
现在有两个问题:
1. 如何快速计算在儿子的答案
2. 往儿子走有可能被卡成 n ^ 2
我们考虑在点分树上“二分”, 这样最多走log层就能到底,找到我们的答案
为了能够快速计算某个点的答案, 我们记录点分树上的点 u的子树到u的答案,sum表示
分情况讨论,画个图容斥一下就可以了:
首先加上u子树内的答案, 用dis1(u) 表示, 其次枚举 u 的祖先 fa
加上 dis1(fa) , 但是u子树内会算重, 于是减去 u的贡献, 用dis2(u)表示,最后还要把所有点从fa移到u
贡献就是 (sum(fa) - sum(u)) * dis(fa, u)
然后修改的时候维护dis1, dis2就可以了
#include<bits/stdc++.h>
#define N 200050
using namespace std;
typedef long long ll;
int read(){
int cnt = 0, f = 1; char ch = 0;
while(!isdigit(ch)){ ch = getchar(); if(ch == '-') f = -1;}
while(isdigit(ch)) cnt = cnt*10 + (ch-'0'), ch = getchar();
return cnt * f;
}
int n, m;
int first[N], nxt[N], to[N], w[N], tot;
void add(int x, int y,int z){
nxt[++tot] = first[x], first[x] = tot;
to[tot] = y; w[tot] = z;
}
ll dis[N], st[N][22];
int lg[N], sign, id[N];
int Siz, siz[N], Maxson[N], rt;
void dfs(int u, int f){
st[++sign][0] = dis[u]; id[u] = sign;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f) continue;
dis[t] = dis[u] + (ll)w[i]; dfs(t, u);
st[++sign][0] = dis[u];
}
}
int vis[N];
ll getdis(int u, int v){
int x = id[u], y = id[v];
if(x > y) swap(x, y);
int t = lg[y-x+1];
return dis[u] + dis[v] - 2 * min(st[x][t], st[y-(1<<t)+1][t]);
}
void getrt(int u, int f){
siz[u] = 1; Maxson[u] = 0;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f || vis[t]) continue;
getrt(t, u); siz[u] += siz[t];
Maxson[u] = max(Maxson[u], siz[t]);
} Maxson[u] = max(Maxson[u], Siz - siz[u]);
if(Maxson[u] < Maxson[rt]) rt = u;
}
int fa[N]; ll sum[N], dis1[N], dis2[N];
vector<pair<int, int> >son[N];
#define mp make_pair
void Divide(int u, int f){
fa[u] = f; vis[u] = 1; int sum = Siz;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(vis[t]) continue;
if(siz[t] > siz[u]) Siz = sum - siz[u];
else Siz = siz[t];
rt = 0; getrt(t, 0); son[u].push_back(mp(t, rt));
Divide(rt, u);
}
}
void update(int u, ll k){
sum[u] += k;
for(int i=u; fa[i]; i = fa[i]){
ll d = getdis(fa[i], u);
dis2[i] += k * d; dis1[fa[i]] += k * d;
sum[fa[i]] += k;
}
}
ll calc(int u){
ll res = dis1[u];
for(int i=u; fa[i]; i = fa[i]){
ll d = getdis(u, fa[i]);
res += dis1[fa[i]] - dis2[i];
res += d * (sum[fa[i]] - sum[i]);
} return res;
}
ll query(int u){
ll ans = calc(u);
for(int i=0; i<son[u].size(); i++){
int t = son[u][i].first, v = son[u][i].second;
ll tmp = calc(t); if(tmp < ans) return query(v);
} return ans;
}
int main(){
n = read(), m = read();
for(int i=1; i<n; i++){
int x = read(), y = read(), z = read();
add(x, y, z); add(y, x, z);
} dfs(1, 0);
for(int i=2; i<=sign; i++) lg[i] = lg[i>>1] + 1;
for(int i=1; (1<<i) <= sign; i++)
for(int j=1; j+(1<<i)-1<=sign; j++)
st[j][i] = min(st[j][i-1], st[j+(1<<(i-1))][i-1]);
Maxson[0] = 1e9; Siz = n; getrt(1, 0); int pre = rt;
Divide(rt, 0); rt = pre;
while(m--){
int u = read(), k = read();
update(u, k);
printf("%lld\n", query(rt));
} return 0;
}
其实,点分树就是一种暴力,但由于树高是log, 所以暴力显现出来很优秀
这道题也其实我们,SCOI2019 DAY1T2 的另外20分做法,就是莫对+点分树
例3:[HNOI2015]开店
类似上一道题,维护前缀和就可以了
#include<bits/stdc++.h>
#define N 500050
using namespace std;
typedef long long ll;
int read(){
int cnt = 0, f = 1; char ch = 0;
while(!isdigit(ch)){ch = getchar(); if(ch == '-') f = -1;}
while(isdigit(ch)) cnt = cnt*10 + (ch-'0'), ch = getchar();
return cnt * f;
}
struct Node{
int x; ll sum;
friend bool operator < (const Node &a, const Node &b){
return a.x < b.x;
}
}; vector<Node> f1[N], f2[N];
int n, m, A, x[N];
int first[N], nxt[N], to[N], w[N], tot;
void add(int x, int y, int z){
nxt[++tot] = first[x], first[x] = tot;
to[tot] = y, w[tot] = z;
}
ll dis[N], st[N][22]; int id[N], sign, lg[N];
void dfs(int u, int f){
st[++sign][0] = dis[u]; id[u] = sign;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f) continue;
dis[t] = dis[u] + (ll)w[i]; dfs(t, u);
st[++sign][0] = dis[u];
}
}
int siz[N], mx[N], rt, Siz, vis[N], fa[N];
void getrt(int u, int f){
siz[u] = 1; mx[u] = 0;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(t == f || vis[t]) continue;
getrt(t, u); siz[u] += siz[t];
mx[u] = max(mx[u], siz[t]);
} mx[u] = max(mx[u], Siz - siz[u]);
if(mx[u] < mx[rt]) rt = u;
}
ll dist(int u, int v){
int x = id[u], y = id[v];
if(x > y) swap(x, y);
int t = lg[y-x+1];
return dis[u] + dis[v] - 2 * min(st[x][t], st[y-(1<<t)+1][t]);
}
void Divide(int u, int f){
fa[u] = f; vis[u] = 1; int sum = Siz;
for(int i=first[u];i;i=nxt[i]){
int t = to[i]; if(vis[t]) continue;
if(siz[t] > siz[u]) Siz = sum - siz[u];
else Siz = siz[t];
rt = 0; getrt(t, 0); Divide(rt, u);
}
}
ll query(int u, int k){
ll res = 0;
for(int i=u; i; i=fa[i]){
int p = lower_bound(f1[i].begin(), f1[i].end(), (Node){k, 0}) - f1[i].begin() - 1;
res += f1[i][p].sum + 1ll * p * dist(u, i);
}
for(int i=u; fa[i]; i=fa[i]){
int p = lower_bound(f2[i].begin(), f2[i].end(), (Node){k, 0}) - f2[i].begin() - 1;
res -= f2[i][p].sum + 1ll * p * dist(u, fa[i]);
} return res;
}
int main(){
n = read(), m = read(), A = read();
for(int i=1; i<=n; i++) x[i] = read();
for(int i=1; i<n; i++){
int x = read(), y = read(), z = read();
add(x, y, z); add(y, x, z);
} dfs(1, 0);
for(int i=2; i<=sign; i++) lg[i] = lg[i>>1] + 1;
for(int i=1; (1<<i) <= sign; i++)
for(int j=1; j+(1<<i)-1<=sign; j++)
st[j][i] = min(st[j][i-1], st[j+(1<<(i-1))][i-1]);
mx[0] = 1e9; Siz = n; getrt(1, 0);
Divide(rt, 0);
for(int i=1; i<=n; i++){
for(int u=i; u; u = fa[u]){
f1[u].push_back((Node){x[i], dist(u, i)});
f2[u].push_back((Node){x[i], dist(fa[u], i)});
}
}
for(int u=1; u<=n; u++){
f1[u].push_back((Node){-1, 0}); f1[u].push_back((Node){2e9, 0});
f2[u].push_back((Node){-1, 0}); f2[u].push_back((Node){2e9, 0});
sort(f1[u].begin(), f1[u].end());
sort(f2[u].begin(), f2[u].end());
for(int i=1; i<f1[u].size(); i++) f1[u][i].sum += f1[u][i-1].sum;
for(int i=1; i<f2[u].size(); i++) f2[u][i].sum += f2[u][i-1].sum;
}
ll ans = 0;
while(m--){
int u = read(), a = read(), b = read();
int L = min((a+ans) % A, (b+ans) % A);
int R = max((a+ans) % A, (b+ans) % A);
ans = query(u, R+1) - query(u, L);
printf("%lld\n", ans);
} return 0;
}
总结一下:
其实点分树就算一种精心策划的暴力,枚举fa的手法比较常见
套上容斥我们就可以知道要维护什么,利用树高的优势使复杂度正确