点分治与点分树 学习笔记

开始营业qwq

点分治

点分治就是把树上的点分治一下,可以批量处理树上和路径相关的问题。

每次根据重心划分,把树分成 O ( log ⁡ ) O(\log) O(log) 个连通块处理答案,然后合并。

Tree

传送门

模板题,把连通块内每一个节点到当前根的距离求出并排序,双指针扫描即可。

这样做会多算一些 LCA 不是当前根的点对,有两个方法解决:分别对每个子节点算出答案进行容斥;或者将每个子节点的子树染成不同颜色,双指针扫描过程中动态维护 [ l . r ] [l.r] [l.r] 区间的每个颜色的数量,颜色与 l l l 相同的不算贡献。

C o d e Code Code:(容斥)

#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=4e4+5;
int n,k;
int head[N],cnt;
struct qwq{
	int v,w,nxt;
}e[N<<1];
inline void add(int u,int v,int w){
	e[++cnt]=qwq{v,w,head[u]},head[u]=cnt;
}
int ans,d[N],tot;
int rt,mx[N],siz[N];
bool vis[N];
void find(int x,int fa,int tot){
	siz[x]=1,mx[x]=0;
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==fa||vis[v]) continue;
		find(v,x,tot),siz[x]+=siz[v];
		mx[x]=max(mx[x],siz[v]);
	}
	mx[x]=max(mx[x],tot-siz[x]);
	if(rt==-1||mx[x]<mx[rt]) rt=x;
}
void get(int x,int fa,int dis){
	d[++tot]=dis;
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v,w=e[i].w;
		if(vis[v]||v==fa) continue;
		get(v,x,dis+w);
	}
}
inline int calc(int x,int dis){
	tot=0,get(x,0,dis);
	sort(d+1,d+tot+1);
	int l=1,r=tot,ans=0;
	while(l<=r){
		if(d[l]+d[r]<=k) ans+=r-l,++l;
		else --r;
	}
	return ans;
}
void solve(int x){
	vis[x]=1,ans+=calc(x,0);
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v,w=e[i].w;
		if(vis[v]) continue;
		ans-=calc(v,w);
	}
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(vis[v]) continue;
		rt=-1,find(v,0,siz[v]);
		solve(rt);
	}
}
signed main(){
	n=read();
	ff(i,2,n){
		int u=read(),v=read(),w=read();
		add(u,v,w),add(v,u,w);
	}
	rt=-1,find(1,0,n);
	k=read();
	solve(rt);
	printf("%d\n",ans);
}

重建计划

传送门

又手残又手残又求错了重心又求错了重心/fn

看见平均值问题先手二分,将所有边权 w w w 都减去 m i d mid mid,问题变为求是否有长度在 [ U , L ] [U,L] [U,L] 之间的路径权值和非负。

使用点分治,递归子树的时候对每个点记录 { d e p , v a l } \{dep,val\} {dep,val}。由于二分+点分治已经有两个 log ⁡ \log log,不能使用线段树等数据结构维护个数,也就不能容斥求解,只能变成存在性问题,即递归每一个子树的时候只算前面统计完的子树对它的贡献。发现当深度递增的时候合法区间 [ L − d e p , U − d e p ] [L-dep,U-dep] [Ldep,Udep] 递减,显然可以单调队列维护,然后注意一下每次更新/清空数组不要使复杂度假掉即可。

为了卡常,可以预处理所有重心及以每个重心为根的子树的最大深度,要是还 T 那就大概是写挂了。

点分树

是个神奇的东西,在带修的求距离等与树形态关系不大的问题时,可以将树重构,使得树高为 O ( log ⁡ ) O(\log) O(log),以支持一些暴力算法。

具体来说就是连接每个点与其子树的重心。重构之后的树上距离和父子关系等都与原树无关,需要在原树上求解。所以一种一般的解法就是对每个点使用两个数据结构维护自己的信息、自己的子树对点分树上的父节点的贡献。

点分树和原树少得可怜的联系:

  • 点分树上两点LCA一定在原树这两点间的路径上
  • 点分树的一个子树在原树上一定联通

点分树 | 震波

传送门

模板题,对每个点 x x x 维护 x x x 子树内到 x x x 的距离相同的点的权值和,以及 x x x 子树内对 x x x 在点分树上父亲 f a fa fa 的距离相同的点的权值和,用两个树状数组 t 1 t1 t1 t 2 t2 t2 即可解决。

由于树高为 log ⁡ n \log n logn,每个节点最多被统计 log ⁡ n \log n logn 次,对每个树状数组按需要的空间用 vector 开点,空间复杂度即为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

对于每次查询,在点分树上暴力向上跳,每次加上父亲的贡献,减去父亲和当前点被重复计算的贡献即可;修改也是暴力上跳修改树状数组,时间复杂度均为 O ( log ⁡ 2 n ) O(\log^2 n) O(log2n)

代码里标出了部分易错细节:

#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=1e5+5;
int n,m,a[N];
int head[N],cnt;
struct qwq{
	int v,nxt;
}e[N<<1];
inline void add(int u,int v){
	e[++cnt]=qwq{v,head[u]},head[u]=cnt;
}
struct BIT{
	vector<int> t;
	int n;
	inline void build(int x){
		n=x+2;
		ff(i,1,n+1) t.push_back(0);
		//n+1保证不会非法访问 
	}
	inline int lowbit(int x){return x&(-x);}
	inline void upd(int x,int y){
		for(int i=x;i<=n;i+=lowbit(i)) t[i]+=y;
	}
	inline int query(int x){
		int res=0;
		for(int i=min(x,n);i;i-=lowbit(i)) res+=t[i];
		return res;
	}
}t1[N],t2[N];
namespace LCA{
	int dep[N],f[N][20];
	inline int lca(int x,int y){
		if(dep[x]<dep[y]) swap(x,y);
		for(int i=18;i>=0;--i) if(dep[f[x][i]]>=dep[y]) x=f[x][i];
		if(x==y) return x;
		for(int i=18;i>=0;--i) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
		return f[x][0];
	} 
	inline int dis(int x,int y){
		return dep[x]+dep[y]-(dep[lca(x,y)]<<1);
	}
	void dfs(int x,int fa){
		dep[x]=dep[fa]+1,f[x][0]=fa;
		ff(i,1,18) f[x][i]=f[f[x][i-1]][i-1];
		for(int i=head[x];i;i=e[i].nxt){
			int v=e[i].v;
			if(v!=fa) dfs(v,x);
		}
	}
}
int siz[N],mx[N],rt,f[N];
vector<int> g;
bool vis[N];
void find(int x,int fa,int tot){
	siz[x]=1,mx[x]=0;
	g.push_back(x);
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==fa||vis[v]) continue;
		find(v,x,tot),siz[x]+=siz[v];;
		mx[x]=max(mx[x],siz[v]);
	}
	mx[x]=max(mx[x],tot-siz[x]);
	if(rt==-1||mx[x]<mx[rt]) rt=x;
}
void dfs(int x,int fa,int rt,int dis){
	t1[rt].upd(dis,a[x]);
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==fa||vis[v]) continue;
		dfs(v,x,rt,dis+1);
	}
}
void init(int x,int fa){
	f[x]=fa,vis[x]=1;
	t1[x].build(g.size()),t2[x].build(g.size());
	if(fa) for(int now:g) t2[x].upd(LCA::dis(fa,now),a[now]);
	//不要忘了if(fa)的特判 
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(!vis[v]) dfs(v,x,x,1);
	}
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].v;
		if(vis[v]) continue;
		rt=-1,g.clear(),find(v,x,siz[v]);
		init(rt,x);
	}
}
inline int query(int x,int k){
	int ans=a[x]+t1[x].query(k),xx=x;
	while(f[x]){
		int d=LCA::dis(f[x],xx);
		if(k>=d) ans+=a[f[x]]+t1[f[x]].query(k-d)-t2[x].query(k-d);
		x=f[x];
	}
	return ans;
}
inline void upd(int x,int y){
	int xx=x;
	while(f[x]){
		int d=LCA::dis(xx,f[x]);
		t2[x].upd(d,y-a[xx]);
		x=f[x];
		t1[x].upd(d,y-a[xx]);
	}
	a[xx]=y;
}
signed main(){
	n=read(),m=read();
	ff(i,1,n) a[i]=read();
	ff(i,2,n){
		int u=read(),v=read();
		add(u,v),add(v,u); 
	}
	LCA::dfs(1,0);
	rt=-1,find(1,0,n);
	init(rt,0);
	int lst=0;
	while(m--){
		int op=read(),x=read()^lst,y=read()^lst;
		if(!op) printf("%d\n",lst=query(x,y));
		else upd(x,y);
	}
}

[ZJOI2015]幻想乡战略游戏

传送门

自己做的时候甚至不知道怎么在点分树上利用树高性质统计答案/kk

考虑u和它的子节点 v v v ( u , v ) = w (u,v)=w (u,v)=w,若补给站从 u u u 移到 v v v v v v 及其子树少移动了 s u m d [ v ] × w sumd[v]\times w sumd[v]×w u u u 及其它子树上的点多移动了 ( s u m d [ u ] − s u m d [ v ] ) × w (sumd[u]-sumd[v])\times w (sumd[u]sumd[v])×w,那么当且仅当 2 s u m d [ v ] > s u m d [ u ] 2sumd[v]>sumd[u] 2sumd[v]>sumd[u] 时移到 v v v 更优,于是同一个点的子节点至多只能选一个,复杂度就是 O ( log ⁡ n ) O(\log n) O(logn) 了。

于是套路性地维护 d 1 [ x ] = ∑ v ∈ x 子树 d i s ( v , x ) × d [ v ] d1[x]=\sum\limits_{v\in x子树}dis(v,x)\times d[v] d1[x]=vx子树dis(v,x)×d[v] d 2 [ x ] = ∑ v ∈ x 子树 d i s ( v , f a [ x ] ) × d [ v ] d2[x]=\sum\limits_{v\in x子树} dis(v,fa[x])\times d[v] d2[x]=vx子树dis(v,fa[x])×d[v] f a [ x ] fa[x] fa[x] 为点分树上 x x x 的父节点),以及 s u m d [ x ] = ∑ v ∈ x 子树 d [ v ] sumd[x]=\sum\limits_{v\in x子树} d[v] sumd[x]=vx子树d[v] 即可。由于每次要从原树的父节点转移到子节点,所以对每个点 x x x 应记录它在点分树上的儿子以及这个儿子是由原树上 x x x 的哪个儿子求重心得到的,根据原树儿子的答案来判断是否转移到点分树的儿子上。

然后懒得重写倍增 LCA 把上一题代码复制过来, d e p dep dep 和点到跟距离应该是两个数组否则 LCA 会错啊,调了一上午/fn/fn/fn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值