动态点分治 / 点分树学习笔记 ---- BZOJ3730 震波 / [ZJOI2017] 幻想乡的战略游戏 / [HNOI2015]开店

点分树就是将每一次的重心连起来, 形成一个深度不超过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个中查询,维护, 从而达到了可观的复杂度


例2:[ZJOI2017] 幻想乡的战略游戏

我们考虑假设当前最优点是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的手法比较常见

套上容斥我们就可以知道要维护什么,利用树高的优势使复杂度正确

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FSYo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值