动态点分治 / 点分树学习笔记 ---- 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
#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 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< 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>1] + 1;
	for(int i=1; (1<

我们经过反思后发现 , 如果每次暴力点分治会把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
#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< >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>1] + 1;
	for(int i=1; (1<

其实,点分树就是一种暴力,但由于树高是log, 所以暴力显现出来很优秀

这道题也其实我们,SCOI2019 DAY1T2 的另外20分做法,就是莫对+点分树


例3:[HNOI2015]开店

类似上一道题,维护前缀和就可以了

#include
#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 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< 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>1] + 1;
	for(int i=1; (1<

总结一下:

其实点分树就算一种精心策划的暴力,枚举fa的手法比较常见

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

你可能感兴趣的:(动态点分治)