点分治学习笔记

怎么感觉这是个玄学算法

基础思路

对于一个以 \(u\) 为根节点的子树内,其所有路径可以分为两类:

1.经过 \(u\) 点的

2.不经过 \(u\) 点的

所以我们可以进行分治,进行深搜,对于每一个点处理经过自己的,然后去深搜解决其子节点

但是这样的算法对于链状的树就炸了,处理自己的 \(O(n)\) , 深搜 \(O(n)\) ,时间复杂度会退化到 \(O(n^2)\)

所以不妨每次找到重心作为根节点,然后再处理

然后我们就得到了淀粉质

例题

这里

考虑对于每个点,应该怎么计算经过它的合法路径长度

我们可以跑出字数内每个点到当前节点的距离,排序后扫一遍双指针,扫出所有距离和在给定区间内的点对

(在代码中,利用了容斥原理,\(counter\) 函数表示所有距离不小于 \(lim\) 的点对)

但是这样会发现同一个子节点的子树内的点也会被计算进去,所以提前减掉即可

代码
#include<bits/stdc++.h>
#define usetime() (double)clock () / CLOCKS_PER_SEC * 1000.0
using namespace std;
typedef long long LL;
const int maxn=1e6+5;
const int inf=2e9+1;
void read(int& x){
	char c;
	bool f=0;
	while((c=getchar())<48) f|=(c==45);
	x=c-48;
	while((c=getchar())>47) x=(x<<3)+(x<<1)+c-48;
	x=(f ? -x : x);
}
int head[maxn],nxt[maxn<<1],e[maxn<<1];
int mp_cnt;
void init_mp(){
	memset(head,-1,sizeof(head));
	mp_cnt=-1;
}
void add_edge(int u,int v){
	e[++mp_cnt]=v;
	nxt[mp_cnt]=head[u];
	head[u]=mp_cnt;
}
int t[maxn];
int n,l,r;
int vis[maxn],s[maxn];
int rt,rti,tot;
LL ans=0;
void bsort(vector<int>& v){//桶排
	int mx=0;
	for(int i=0;i<(int)v.size();i++){
		++t[v[i]];
		mx=max(mx,v[i]);
	}
	v.clear();
	for(int i=0;i<=mx;i++){
		while(t[i]){
			--t[i];
			v.push_back(i);
		}
	}
}
void get_rt(int u,int fa){//寻找子树重心
	int p=0;
	s[u]=1;
	for(int i=head[u];~i;i=nxt[i]){
		int v=e[i];
		if(v==fa||vis[v]) continue;
		get_rt(v,u);
		s[u]+=s[v];
		p=max(p,s[v]);
	}
	p=max(p,tot-s[u]);
	if(p<=rti) rt=u,rti=p;
}
vector<int> v1,v2;
void get_dis(int u,int fa,int d){//获取子树内所有距离
	v1.push_back(d),v2.push_back(d);
	for(int i=head[u];~i;i=nxt[i]){
		int v=e[i];
		if(v==fa||vis[v]) continue;
		get_dis(v,u,d+1);
	}
}
LL counter(vector<int>& v,int lim){//双指针
	LL p=0,sum=0;
	int r=(int)v.size()-1;
	for(int i=0;i<(int)v.size();i++) sum+=v[i];
	for(int i=0;i<(int)v.size();i++){
		sum-=v[i];
		while(r>=0&&v[i]+v[r]>lim) sum-=v[r--];
		if(r<i) break;
		p+=sum+1ll*(r-i)*v[i];
	}
	return p;
}
void work(int u){//计算答案
	v1.clear();
	v1.push_back(0);
	for(int i=head[u];~i;i=nxt[i]){
		int v=e[i];
		if(vis[v]) continue;
		v2.clear();
		get_dis(v,u,1);
		bsort(v2);
		ans-=counter(v2,r)-counter(v2,l-1);
	}
	bsort(v1);
	ans+=counter(v1,r)-counter(v1,l-1);
}
void dfs(int u){
	vis[u]=1,work(u);
	for(int i=head[u];~i;i=nxt[i]){
		int v=e[i];
		if(vis[v]) continue;
		rti=inf,tot=s[v];
		get_rt(v,u);
		dfs(rt);
	}
}
int main(){
	read(n),read(l),read(r);
	int u;
	init_mp();
	for(int i=2;i<=n;i++){
		read(u);
		add_edge(u,i),add_edge(i,u);
	}
	tot=n,rti=inf;
	get_rt(1,1);
	dfs(rt);
	printf("%lld",ans);
	return 0;
}
//^o^

你可能会发现在求重心时的子树大小并不是正确的,因为其根节点是不同的

但是这并不影响正确性与时间复杂度,详见这里

所以你会发现,这里不能使用 \(oi\;wiki\)这种方法来判断重心

由于使用了桶排,这段代码的时间复杂度为 \(O(n\;log\;n)\)

posted @ 2025-08-20 15:55  huangems  阅读(20)  评论(0)    收藏  举报