点分治学习笔记

怎么感觉这是个玄学算法

基础思路

对于一个以 \(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^

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

但是这并不影响正确性与时间复杂度,详见[这里]( https://liu-cheng-ao.blog.uoj.ac/blog/2969#:~:text=可以明显发现,当 K 和 K 处于上一层重心 O 的同一子树时,传入的点数是错误的。 这是否会对时间复杂度造成影响呢? ,%E4%BC%A0%E5%85%A5%20%E7%9A%84%E7%82%B9%E6%95%B0%E5%80%BC%E4%B8%BA%20n%E3%80%82%20%E5%8F%AF%E4%BB%A5%E4%B8%BE%E5%87%BA%E5%8F%8D%E4%BE%8B%EF%BC%9A%E8%80%83%E8%99%91%E8%BF%99%E6%A0%B7%E4%B8%80%E6%A3%B5%E6%A0%91%EF%BC%9A%E7%94%B1%E9%87%8D%E5%BF%83%E5%92%8C%E7%AB%AF%E7%82%B9%E4%B8%8E%E4%B9%8B%E7%9B%B8%E8%BF%9E%E7%9A%84%E4%B8%89%E6%9D%A1%E9%95%BF%20n%201%203%20%E7%9A%84%E9%93%BE%E7%BB%84%E6%88%90%E3%80%82)

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

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

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