点分治&点分树 复习

点分治&点分树复习

点分治

点分治是处理树上路径问题的一种通用的方式,是树分治的一种。

从最一般的问题来入手,有这样一个经典的问题:

例题1

Tree

给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量。

如果我们直接求所有点对,那么复杂度是\(O(n^2)\) 的,对于这道题 \(n\le4\times10^4\)来说,是无法接受的。

而这道题的问题只是求距离的数量,和具体的点没关系,也就是说,我们只需要知道距离的集合即可。

想到一个点一个点的考虑,确定一个点,然后枚举所有的子树,求出所有到这个点的距离的集合,在这些距离中,已经有满足\(dis\le k\) 的,且还有可以组合起来仍然\(\le k\) 的,先不管它们怎么组合,我们先考虑通过这样的方法我们做到了什么,这样的操作相当于通过这个点能够找到的距离已经全部被处理过了,且经过这个点的两个不同子树内的点也被考虑过了,所以我们可以只单独的考虑每个子树内的内容,而不用再考虑和这个点有关的路径,然后我们可以递归处理每个子树内的情况,当然这样的操作还不能降低复杂度,因为递归下去可能遇到的还是一颗大小近似\(n\)的子树,那怎么办?遇到问题就解决问题,既然可能遇到一颗大小近似\(n\)的子树,那就提前让确定的这个点的子树都不超过\(n/2\) 即我们找到树的中心,然后如果我们能够\(O(n)\) 解决我们组合一个点所搜索到的\(dis\) ,那么我们就可以\(T(n)=T(n/2)+n\) 即复杂度\(O(nlogn)\)的得到最终的结果;即使是\(O(nlogn)\)的解决每次的面临的问题,最终的复杂度也只是\(O(nlog^2n)\)

这就是点分治,可以概括为三个步骤:

1 找到树的重心,统计有关重心的路径信息。

2 统计所有路径信息,不同子树间进行归并后计算,直接和重心相连的路径直接计算。

3 删除重心,递归到子树做同样的事情。

听起来是不难的,确实,分治的思想是不困难的,对于点分治来说困难的是:中间归并的操作。

这道模板题就需要点小小的技巧,首先是对于和重心相连的点,\(dis\le k\) 的直接计入答案,然后将所有子树的dis计入到一个数组中,将数组排序之后,考虑使用双指针,一个在头,一个在尾,二者相加如果\(\le k\) 的话,那么所有的左指针前的点和右指针这个点的dis之和都是\(\le k\) 的,然后左指针右移;反之,如果二者相加大于k,那么右指针应该向左移动;正确性:我们是从边界开始考虑的,所以说一开始的\(l\)如果不能满足\(r\) ,那么\(r\) 只能左移,这个时候\(l\) 如果满足了新的\(r\),那么也一定能够满足向左移动的\(r\) ,然后我们可以尝试让\(l\) 向右移动看看能不能满足更多的\(l\) ,就这样,这样是满足一个单调性的,而两个指针如果相遇了,那么只要\(r\) 一直向左推就好。还有一个问题就是,这个过程中可能出现同一颗子树到重心的dis,它们是不能被组合的,所以我们在搜索每一颗子树的dis的时候,就先对着这颗子树内的dis进行一波\(\le k\)的组合的统计,然后直接让ans减去这些答案即可,收工!。
具体代码如下

点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register
typedef long long ll;
using namespace std;
inline int qr(){
	int x=0,f=0;char ch=0;
	while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return f?-x:x;
}
const int maxn=2e4+100;
int n,m;
struct node{
	int next,to,cost;
}edge[maxn];
int head[maxn],_tot;
bool o[maxn];
int p[maxn],q[maxn];//p存当前所有子树内的点到重心的距离,q数组存当前子树内所有点到重心的距离 
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;;head[u]=_tot++;}
int get_size(int x,int fa){//求得子树大小  
	if(o[x]) return 0;
	int res=1;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		res+=get_size(y,x);
	}
	return res;
}
int get_wc(int x,int fa,int tot,int &wc){//求树的伪重心  tot表示当前节点的子树大小  
	if(o[x]) return 0;
	int sum=1,maxx=0;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		int t=get_wc(y,x,tot,wc);
		maxx=max(maxx,t);
		sum+=t;
	}
	maxx=max(maxx,tot-sum);
	if(maxx<=tot/2) wc=x;
	return sum;//返回子树的大小 
}
void get_dis(int x,int fa,int dis,int &qt){//求到重心的距离 
	if(o[x]) return ;
	q[qt++]=dis;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		get_dis(y,x,dis+edge[i].cost,qt);
	}
}
int get(int a[],int k){//用双指针算法求一个序列中两数相加小于等于k的数的数量 
	sort(a,a+k);
	int  res=0;
	for(reg int i=k-1,j=-1;i>=0;i--){
		while(j+1<i&&a[j+1]+a[i]<=m) j++;
		j=min(j,i-1);
		res+=j+1;
	}
	return res;
}
int calc(int x){
	if(o[x]) return 0;
	int res=0;
	get_wc(x,-1,get_size(x,-1),x);
	o[x]=1;//删除重心 
	int len=0;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to,qt=0;
		get_dis(y,-1,edge[i].cost,qt);
		res-=get(q,qt);
		for(reg int k=0;k<qt;k++){
			if(q[k]<=m) res++;
			p[len++]=q[k];
		}
	}
	res+=get(p,len);
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		res+=calc(y);
	}
	return res;
}
int main(){
//	freopen("poj1741_tree.in","r",stdin);
//	freopen("poj1741_tree.out","w",stdout);
	n=qr();
	memset(head,-1,sizeof(head));
	memset(o,0,sizeof(o));
	_tot=0;
	for(reg int i=1;i<n;i++){
		int u=qr()-1,v=qr()-1,l=qr();
		add(u,v,l);add(v,u,l);
	}
	m=qr();
	printf("%d\n",calc(0));
	return 0;
}
/*
点分治 对树上的点进行分治,递归归并求解以达到nlog^2n 的复杂度 
针对一棵树先找到树的伪重心(任意子树大小小于1/2即可),然后针对每一棵子树递归
继续求解,然后对子树内容进行归并求解 
*/

例题2

再介绍一道 :Race/权值

给定一颗大小为 \(N\) 的树,求权值为 \(k\) 的经过边数最小的路径,不存在则输出-1,\(N\le2\times10^{5},K\le10^6\)

考虑DP,\(f[i]\) 表示权值为 \(i\) 的路径最短边数是多少,然后我们按照类似的过程:

枚举所有方案 ,首先 找到树的重心,然后开始分治
1 两个点都在某个子树内,递归下去求解
2 有一个点是重心,求每个点到重心的距离即可
3 两个点分别在不同的子树内,开一个桶,i存到重心的距离是i的所有点中边数最小的点
所以枚举一个子树内有dis==x,考虑其它子树的桶内k-x的点即可
和上一道题一模一样,基本都是考虑这三种情况 。

点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register 
#define X first
#define y second
typedef long long ll;
using namespace std;
inline int qr(){
	int x=0,f=0;char ch=0;
	while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return f?-x:x;
}
const int maxn=2e5+100;
const int M=1e6+100;
const int inf=0x3f3f3f3f;
int n,m;
struct node{
	int next,to,cost;
}edge[maxn<<1];
int head[maxn],_tot;
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;head[u]=_tot++;}
pair<int,int>q[maxn],p[maxn];
int f[M];
bool o[maxn];
int ans;
int get_size(int x,int fa){
	if(o[x]) return 0;
	int res=1;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		res+=get_size(y,x);
	}
	return res;
}
int get_wc(int x,int fa,int tot,int &wc){
	if(o[x]) return 0;
	int sum=1,maxx=0;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa ) continue;
		int t=get_wc(y,x,tot,wc);
		maxx=max(maxx,t);
		sum+=t;
	}
	maxx=max(maxx,tot-sum);//父亲也得算  
	if(maxx<=tot/2) wc=x;
	return sum;
}
void get_dis(int x,int fa,int dis,int cnt,int &qt){//cnt表示经过的边的数量 
	if(o[x]||dis>m) return ;//小剪枝 
	q[qt++]=make_pair(dis,cnt);
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		get_dis(y,x,dis+edge[i].cost,cnt+1,qt);
	}
}
void calc(int x){
	if(o[x])return;
	get_wc(x,-1,get_size(x,-1),x);
	o[x]=1;
	int len=0;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to,qt=0;
		get_dis(y,-1,edge[i].cost,1,qt);
		for(reg int k=0;k<qt;k++){//归并 
			pair<int,int> t =q[k]; 
			if(t.X==m) ans=min(ans,t.y);//对于直接等于m的点,比较ans 
			ans=min(ans,f[m-t.X]+t.y);//否则去桶里找 
			p[len++]=t;//统计一下整个联通块的合法点  
		}
		for(reg int k=0;k<qt;k++){
			pair<int,int >t =q[k];
			f[t.X]=min(f[t.X],t.y);
		}//把自己放在桶里 
	}
	for(reg int i=0;i<len;i++){//清空 
		f[p[i].X]=inf;
	}
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		calc(y);
	}
}
int main(){
//	freopen("test.in","r",stdin);
//	freopen("test.out","w",stdout);
	n=qr();m=qr();
	memset(f,0x3f,sizeof(f));
	ans=inf;
	memset(head,-1,sizeof(head));
	for(reg int i=1;i<n;i++){
		int u=qr(),v=qr(),l=qr();
		add(u,v,l);add(v,u,l);
	}
	calc(0);
	if(ans==inf) ans=-1;
	printf("%d\n",ans);
	return 0;
}

例题3

看一道比较复杂的

[SPOJ1825] 免费旅行II

在两周年纪念日的旅行之后,在第三年,旅行社SPOJ又一次踏上的打折旅行的道路。

这次旅行是ICPC岛屿上进行的,一个位于太平洋上,不可思议的小岛。我们列出了N个地点(编号从1到N)供旅客游览。这N个点由N-1条边连成一个树,每条边都有一个权值,这个权值可能为负。我们可以选择两个地点作为旅行的起点和终点。

由于当地正在庆祝节日,所以某些地方会特别的拥挤(我们称这些地方为拥挤点)。旅行的组织者希望这次旅行最多访问K个拥挤点。同时,我们希望我们经过的道路的权值和最大。

其中\(n\le 2\times10^5\),M,K与N同阶

确认是点分治之后,主要考虑如何利用信息进行归并,我们需要的是路径的长度(经过的边数)以及路径上的点的数量,需要达成的目的是满足点的数量的同时取路径最长,和上一道题类似,上一道是满足权值求最小边,所以我们设置的状态是 \(f[i]\) 表示权值为 \(i\) 然后状态的属性是 \(max\) 边的数量,那么这道题仍然考虑DP,设 \(f[i]\) 表示当前子树内经过 \(i\) 个拥挤点路径的最长值,\(g[i]\) 表示已经遍历过的子树内经过 \(i\) 个拥挤点路径的最长值,那么我们在确定好重心之后,对于每个\(f[i]\) 考虑已经处理过的子树内$0 \(~\)K-i$ 的状态,二者相加来更新ans,怎么考虑这个\(0\)~\(K-i\) 的状态?本人采用了ST表的办法,这样每次询问就是\(O(1)\)的,统计过答案之后更新 \(g\) 数组(再次建表),每次建表是\(O(nlogn)\) 所以总复杂度\(O(nlog^2n)\)

可能ST表复杂度比较高。而题目\(N\le2\times10^5\) ,所以\(O(nlog^2n)\)太大了,所以代码里对于\(K==M\)的部分采用了树形DP。

点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register 
typedef long long ll;
using namespace std;
inline int qr(){
	int x=0,f=0;char ch=0;
	while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return f?-x:x;
}
const int maxn=2e5+100;
int n,K,M;//点数,忍受数,拥挤点数 
struct node{
	int next,to,cost;
}edge[maxn<<1];
int head[maxn],_tot;
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;head[u]=_tot++;}
ll f[maxn],g[maxn];
ll F[5000000][22];
bool o[maxn];
bool sb[maxn];//拥挤的点 
int mx,maxs;
ll ans;
int get_size(int x,int fa){
	if(o[x]) return 0;
	int res=1;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		res+=get_size(y,x);
	}
	return res;
}
int get_wc(int x,int fa,int tot,int &wc){
	if(o[x]) return 0;
	int sum=1,maxx=0;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		int t=get_wc(y,x,tot,wc);
		sum+=t;
		maxx=max(maxx,t);
	}
	maxx=max(maxx,tot-sum);
	if(maxx<=tot/2) wc=x;
	return sum;
}
int cnt;
ll fp,G;
void get_dis(int x,int fa,ll dis){//cnt表示经过了几个拥挤点 
	fp=max(fp,dis);
	if(o[x]) return ;
	if(sb[x]&&cnt>=K) return;
	if(sb[x]) cnt++,f[cnt]=max(dis,f[cnt]);
	else f[cnt]=max(f[cnt],dis);
	maxs=max(cnt,maxs);
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		get_dis(y,x,dis+edge[i].cost);
	}
	if(sb[x]) cnt--;
}

void st(){
    for(int i = 0; i <= mx+1; i ++) F[i][0] = g[i]; 
    int t = log(n) / log(2) + 1;
    for(int j = 1; j < 20; j ++){
        for(int i = 0; i <= mx - (1 << j) +1; i ++){
            F[i][j] = max(F[i][j-1],F[i + (1 << (j - 1))][j - 1]);
        }
    }
}
int query(int x, int y)
{
    int t = log(abs(y-x +1))/ log(2);
    int a = F[x][t];
    int b = F[y - (1 << t) +1][t];
    return max(a,b);
}
ll dp1[maxn],dp2[maxn];
void calc(int x){
	if(o[x]) return;
	if(K==M) {//特判树形DP
		o[x]=1;G=0;
		for(reg int i=head[x];~i;i=edge[i].next){
			int y=edge[i].to;
			if(o[y]) continue;
			calc(y);
			if(dp1[x]<dp1[y]+edge[i].cost){
				dp2[x]=dp1[x];dp1[x]=dp1[y]+edge[i].cost;
			}else if(dp2[x]<dp1[y]+edge[i].cost){
				dp2[x]=dp1[y]+edge[i].cost;
			}
			ans=max(dp1[x]+dp2[x],ans);
		}
		return;
	}
	get_wc(x,-1,get_size(x,-1),x);
	o[x]=1;mx=0;
	if(sb[x])K--;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(o[y]) continue;
		maxs=0;cnt=0;
		get_dis(y,x,edge[i].cost);
		mx=max(maxs,mx);
		st();//重新建表
		for(reg int j=0;j<=maxs;j++){
			ans=max(ans,f[j]);
			ans=max(ans,f[j]+query(0,min(mx,max(0,K-j))));//查询区间最大值
		}
		for(reg int j=0;j<=maxs;j++) {
			g[j]=max(g[j],f[j]);f[j]=0;
		}
	}
	if(sb[x]) K++;
	for(reg int i=0;i<=mx;i++) g[i]=0;//清空 
	for(reg int i=head[x];~i;i=edge[i].next) calc(edge[i].to);
}
int main(){
	freopen("freetourII.in","r",stdin);
	freopen("freetourII.out","w",stdout);
	n=qr(),K=qr(),M=qr();
	memset(head,-1,sizeof(head));
	for(reg int i=1;i<=M;i++){
		int x=qr(); sb[x]=1;
	}
	for(reg int i=1;i<n;i++){
		int u=qr(),v=qr(),l=qr();
		add(u,v,l);add(v,u,l);
	}
	calc(1);
	printf("%lld",ans);
	return 0;
}
/*
找到一条最长路径,其经过的拥挤点不超过K个
设f[i]为经过i个拥挤点,到达重心的最长距离,每次合并所有子树的时候只需要计算f[i+j]中最大的即可 i+j<=K 
*/

到这里基本印证了先前的说法,点分治的模板框架并不难,每道题的核心都在于归并。

例题4

shopping

马上就是小苗的生日了,为了给小苗准备礼物,小葱兴冲冲地来到了商店街。商店街有 \(n\)个商店,并且它们之间的道路构成了一棵树的形状。

\(i\) 个商店只卖第 \(i\)种物品,小苗对于这种物品的喜爱度是 \(w_i\),物品的价格为$ c_i$,物品的库存是 \(d_i\)。但是商店街有一项奇怪的规定:如果在商店 \(u,v\)买了东西,并且有一个商店 \(p\)\(u\)\(v\)的路径上,那么必须要在商店 \(p\)买东西。小葱身上有 \(m\)元钱,他想要尽量让小苗开心,所以他希望最大化小苗对买到物品的喜爱度之和。

这种小问题对于小葱来说当然不在话下,但是他的身边没有电脑,于是他打电话给同为OI选手的你,你能帮帮他吗?

\(N\le500;m,w_i,c_i\le 4000;d_i\le100;c_i\le m;\) 多测 \(T\le5\)

假如问题放在序列上,那么这道题就是一个简单的多重背包,放在树上就需要考虑树形背包,一般的树形背包是\(f[i]\)表示子树下的状态,但这题限制了选择了两个点之后,两点之间的路径上所有商店都至少要选择一个商品,这样就出现了"树上路径"问题,考虑另一种状态表示,\(f[i][j]\)表示考虑了dfn序 \(i\) ~ \(n\) 的物品,背包占用为 \(j\) 的最大价值,我们先肯定我们一定要选择重心,不选择重心的情况我们递归考虑,然后我们求出从重心出发每个点的dfn序,并每个点记录一个out表示子树内dfn序的最大值,然后我们倒序考虑dfn序,即从深到浅考虑背包,并且将每个点必选一个的状态强行放入,这样我们每个点就能够"拓扑的"考虑到它被强迫选择的情况(因为一定要选择重心,所以假如有选择了更深处的点,那么一定会选择较浅的点),并更新背包的状态,当然对于一个子树可以完全不选择以达到更优,这个时候我们的out就可以帮助我们将\(f\) 带回到采用这个子树前的状态,然后"纠正"背包的状态,背包需要采用二进制分组来优化,这样复杂度就可以做到 \(O(nm\log\ d \ log\ n)\) 了。

点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register 
#define mp make_pair
typedef long long ll;
using namespace std;
inline int qr(){
	int x=0,f=0;char ch=0;
	while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return f?-x:x;
}
const int N=4140;
int n,m,ans;
int w[N],c[N],d[N];
int dfn[N],dfncnt,out[N];
int f[N][N];//考虑了dfn序列i~n的背包 
bool o[N];
struct node{
	int next,to;
}edge[N<<1];
int head[N],_tot;
inline void add(int u,int v){
	edge[_tot]=(node){head[u],v},head[u]=_tot++;
}
pair<int,int>p[N];
int get_size(int x,int fa){
	if(o[x]) return 0;
	int sum=1;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		sum+=get_size(y,x);
	}	
	return sum;
}
int get_wc(int x,int fa,int tot,int &wc){
	if(o[x]) return 0;
	int res=1,maxson=-1;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		int c=get_wc(y,x,tot,wc);
		res+=c;
		maxson=max(c,maxson);
	}
	maxson=max(tot-maxson,maxson);
	if(maxson<=tot/2) wc=x;
	return res;
}
void get_dfn(int x,int fa){
	if(o[x]) return;
	dfn[++dfncnt]=x;
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		if(y==fa) continue;
		get_dfn(y,x);
	}out[x]=dfncnt;
}
void calc(int x){
	if(o[x]) return;
	get_wc(x,0,get_size(x,0),x);
	dfncnt=0;
	get_dfn(x,0);
	o[x]=1;
	for(reg int i=dfncnt;i;i--){
		int s=d[dfn[i]]-1,num=0;
		for(reg int j=1;j<=s;s-=j,j<<=1) p[++num]={w[dfn[i]]*j,c[dfn[i]]*j};//二进制分组 
		if(s) p[++num]={w[dfn[i]]*s,c[dfn[i]]*s};
		for(reg int j=m;j>=c[dfn[i]];j--){//强行规定必选一个i的状态 
			f[i][j]=f[i+1][j-c[dfn[i]]]+w[dfn[i]];
		}
		for(reg int k=1;k<=num;k++){//再考虑之前的所有物品 
			for(reg int j=m;j>=p[k].second;j--){
				f[i][j]=max(f[i][j],f[i][j-p[k].second]+p[k].first);
			}
		}
		for(reg int j=0;j<=m;j++){//不选择这颗子树,考虑子树外,相当于尝试纠正刚刚考虑强塞这颗子树的点的错误状态 
			f[i][j]=max(f[i][j],f[out[dfn[i]]+1][j]);
		}
	}
	ans=max(ans,f[1][m]);
	for(reg int i=0;i<=dfncnt;i++){
		for(reg int j=0;j<=m;j++){
			f[i][j]=0;
		}
	}
	for(reg int i=head[x];~i;i=edge[i].next){
		int y=edge[i].to;
		calc(y);
	}
}
void clear(){
	memset(head,-1,sizeof head);
	memset(o,0,sizeof o);
	_tot=ans=dfncnt=0;
}
int main(){
	freopen("shopping.in","r",stdin);
	freopen("shopping.out","w",stdout);	 
	int T=qr();
	while(T--){
		clear();
		n=qr(),m=qr();
		for(reg int i=1;i<=n;i++) w[i]=qr();
		for(reg int i=1;i<=n;i++) c[i]=qr();
		for(reg int i=1;i<=n;i++) d[i]=qr();	
		for(reg int i=1;i<n;i++){
			int u=qr(),v=qr();
			add(u,v);add(v,u);
		}
		calc(1);
		printf("%d\n",ans);
	}
	return 0;
}


点分治小总结

*思考,在什么情况下使用点分治而不是其他的大数据结构,树形DP之类的算法。

1 树上路径问题,求点对之类的问题,这类问题比较容易点分。

2 对于确定一个点之后,其它点和它的关系所产生的信息能够在\(O(nlogn)\)或者一个再乘\(logn\)不会T的复杂度之内解决的(一般来说不会是\(n^2\),因为 \(n^2\) 大概率是暴力统计点对),且不需要再考虑这个点的信息的问题。

点分树

(待更)

posted @ 2021-05-27 10:46  explorerxx  阅读(127)  评论(0编辑  收藏  举报