K-D Tree

基本思想

放在前面:该数据结构的时间复杂度很玄学,一般用于骗分,当然不排除作为正解的可能性。

下面简称该数据结构为 \(KDT\).

\(KDT\) 是一种可以高效处理 \(k\) 维空间信息的数据结构。换句话说,他是维护 \(k\) 维空间 \(n\) 个点的一种平衡树。

一般在算法竞赛中,我们需要维护的是 \(2DT\)

下面简单说一下轮换划分法:

我们假设当前以第 \(x\) 维进行划分,则下一次划分以第 \((x+1)\bmod k\) 维进行。

事实上,过程是这样的:

  • 以当前的关键字选取中位数,对应点作为该子树的根节点。

  • 交替选择不同关键字(就是轮换划分法,上面说过了)。

  • 将父亲节点向两个儿子连边。

然后看一下 \(KDT\) 有什么性质:

  • 树的每一层都依据同一个关键字进行划分(比较显然,看一下上面的过程)。

  • \(2DT\) 中,一棵子树可以划分出一个矩形。

然后我们就可以看一下题了,因为 \(KDT\) 的大部分东西都要就题论题。

简单题

神奇玩意,强制在线卡掉了 \(cdq\) 分治,空间限制卡掉了所有树套树,所以只能使用 \(KDT\) 来做。

我们在后面要用到一个不平衡系数,一般取 \([0.7,0.75]\),下面我取的是 \(0.75\)

插入

首先说一下插入。如果当前插入点比当前点在当前统计方式下更小就插入到左子树,否则插入到右子树。

然后如果发现当前点没有东西,直接插入到这个点上。最后记得上传并且判断是否不平衡,如果不平衡则需要重构。代码:

void ins(int &u,node t,int d){
	if(!u){//找到插入点
		u=get_new();
		tr[u].l=tr[u].r=0;//目前没有子树
		tr[u].p=t;
		pushup(u);
		return;
	}
	if(t.num[d]<=tr[u].p.num[d])ins(tr[u].l,t,(d+1)%2);
	else ins(tr[u].r,t,(d+1)%2);
	pushup(u);
	check(u,d);//判断是否不平衡
}

判断和重构

然后我们说一下判断和重构。判断很好说,就是如果当前点的一个子树的大小已经很大了,我们就需要重构。

接着说一下怎么重构。我们要先找出按照当前关键字的中间值,这个可以 \(O(n)\) 完成,使用 nth_element 即可。

然后我们就把当前点赋成这个中间值的点,然后递归左右子树。这是非常好理解的,所以放一下代码:

void slap(int u){
	if(!u)return;
	p[++now]=tr[u].p;//收集子树中的点
	can[++cnt]=u;//这个点要被重构,换句话说没用了,放进回收站
	slap(tr[u].l);
	slap(tr[u].r);
}
int rebuild(int l,int r,int d){
	if(l>r)return 0;//没有点了
	int mid=l+r>>1;
	int u=get_new();//开个点
	if(d==0)nth_element(p+l,p+mid,p+r+1,cmpx);
	else nth_element(p+l,p+mid,p+r+1,cmpy);//找到中间值
	tr[u].p=p[mid];//当前点为中间值
	tr[u].l=rebuild(l,mid-1,(d+1)%2);
	tr[u].r=rebuild(mid+1,r,(d+1)%2);//递归两边
	pushup(u);//然后别忘了上传
	return u;
}
void check(int &u,int d){
	if(tr[tr[u].l].siz>alp*tr[u].siz||tr[tr[u].r].siz>alp*tr[u].siz){//如果某一棵子树大小过大
		now=0;
		slap(u);//记录子树里的所有点
		u=rebuild(1,tr[u].siz,d);//需要重构
	}
}

查询

这里要用一个性质,我们上面提到了在 \(2DT\) 中一个子树划分出一个矩形。

所以我们用四个值代表这个子树的上下左右边界。显然是很好判断这个子树代表的矩形和查询矩形的关系的。如果被包含或者完全无交我们就可以直接返回了。

然后如果仅仅相交,我们先判断当前点是否被包含,以确定是否加上他。然后递归两个子树重复这一过程。

完全无交和被包含的函数非常好理解,看一看就行了。代码:

bool inrange(int x_1,int y_1,int x_2,int y_2,int X_1,int Y_1,int X_2,int Y_2){
	return x_1<=X_1&&x_2>=X_2&&y_1<=Y_1&&y_2>=Y_2;
}
bool outrange(int x_1,int y_1,int x_2,int y_2,int X_1,int Y_1,int X_2,int Y_2){
	return x_1>X_2||x_2<X_1||y_1>Y_2||y_2<Y_1;
}
int qry(int u,int x_1,int y_1,int x_2,int y_2){
	if(!u)return 0;//没有开的点,不造成贡献并且不能递归,否则死循环
	if(inrange(x_1,y_1,x_2,y_2,tr[u].mn[0],tr[u].mn[1],tr[u].mx[0],tr[u].mx[1])){
		return tr[u].sum;//包含
	}
	if(outrange(x_1,y_1,x_2,y_2,tr[u].mn[0],tr[u].mn[1],tr[u].mx[0],tr[u].mx[1])){
		return 0;//无交
	}
	int res=0;
	if(inrange(x_1,y_1,x_2,y_2,tr[u].p.num[0],tr[u].p.num[1],tr[u].p.num[0],tr[u].p.num[1])){
		res+=tr[u].p.val;//要不要加上当前点
	}
	res+=qry(tr[u].l,x_1,y_1,x_2,y_2)+qry(tr[u].r,x_1,y_1,x_2,y_2);//递归子树(其实就是小矩形)
	return res;
}

代码(完整版)

#include<bits/stdc++.h>
#define int long long
#define N 200005
#define alp 0.75
using namespace std;
struct node{
	int num[2],val;//两个坐标和权值
	node(){};
	node(int x,int y,int v){
		num[0]=x;
		num[1]=y;
		val=v;
	}
}p[N];
struct tree{
	int l,r,mn[2],mx[2],sum,siz;
	node p;
}tr[N];
int n,tot,rt,cnt,now,can[N<<2];
bool cmpx(node a,node b){//轮换划分的按两个坐标排序
	return a.num[0]<b.num[0];
}
bool cmpy(node a,node b){
	return a.num[1]<b.num[1];
}
int get_new(){
	if(cnt)return can[cnt--];
	return ++tot;
}
void pushup(int u){
	for(int i=0;i<2;i++){
		tr[u].mn[i]=tr[u].mx[i]=tr[u].p.num[i];//初始值就是点的坐标
		if(tr[u].l){
			tr[u].mn[i]=min(tr[u].mn[i],tr[tr[u].l].mn[i]);
			tr[u].mx[i]=max(tr[u].mx[i],tr[tr[u].l].mx[i]);
		}
		if(tr[u].r){
			tr[u].mn[i]=min(tr[u].mn[i],tr[tr[u].r].mn[i]);
			tr[u].mx[i]=max(tr[u].mx[i],tr[tr[u].r].mx[i]);
		}
	}
	tr[u].sum=tr[tr[u].l].sum+tr[tr[u].r].sum+tr[u].p.val;
	tr[u].siz=tr[tr[u].l].siz+tr[tr[u].r].siz+1;//平衡树基本pushup
}
void slap(int u){
	if(!u)return;
	p[++now]=tr[u].p;//收集子树中的点
	can[++cnt]=u;//这个点要被重构,换句话说没用了,放进回收站
	slap(tr[u].l);
	slap(tr[u].r);
}
int rebuild(int l,int r,int d){
	if(l>r)return 0;//没有点了
	int mid=l+r>>1;
	int u=get_new();//开个点
	if(d==0)nth_element(p+l,p+mid,p+r+1,cmpx);
	else nth_element(p+l,p+mid,p+r+1,cmpy);//找到中间值
	tr[u].p=p[mid];//当前点为中间值
	tr[u].l=rebuild(l,mid-1,(d+1)%2);
	tr[u].r=rebuild(mid+1,r,(d+1)%2);//递归两边
	pushup(u);//然后别忘了上传
	return u;
}
void check(int &u,int d){
	if(tr[tr[u].l].siz>alp*tr[u].siz||tr[tr[u].r].siz>alp*tr[u].siz){//如果某一棵子树大小过大
		now=0;
		slap(u);//记录子树里的所有点
		u=rebuild(1,tr[u].siz,d);//需要重构
	}
}
void ins(int &u,node t,int d){
	if(!u){//找到插入点
		u=get_new();
		tr[u].l=tr[u].r=0;//目前没有子树
		tr[u].p=t;
		pushup(u);
		return;
	}
	if(t.num[d]<=tr[u].p.num[d])ins(tr[u].l,t,(d+1)%2);
	else ins(tr[u].r,t,(d+1)%2);
	pushup(u);
	check(u,d);//判断是否不平衡
}
bool inrange(int x_1,int y_1,int x_2,int y_2,int X_1,int Y_1,int X_2,int Y_2){
	return x_1<=X_1&&x_2>=X_2&&y_1<=Y_1&&y_2>=Y_2;
}
bool outrange(int x_1,int y_1,int x_2,int y_2,int X_1,int Y_1,int X_2,int Y_2){
	return x_1>X_2||x_2<X_1||y_1>Y_2||y_2<Y_1;
}
int qry(int u,int x_1,int y_1,int x_2,int y_2){
	if(!u)return 0;//没有开的点,不造成贡献并且不能递归,否则死循环
	if(inrange(x_1,y_1,x_2,y_2,tr[u].mn[0],tr[u].mn[1],tr[u].mx[0],tr[u].mx[1])){
		return tr[u].sum;//包含
	}
	if(outrange(x_1,y_1,x_2,y_2,tr[u].mn[0],tr[u].mn[1],tr[u].mx[0],tr[u].mx[1])){
		return 0;//无交
	}
	int res=0;
	if(inrange(x_1,y_1,x_2,y_2,tr[u].p.num[0],tr[u].p.num[1],tr[u].p.num[0],tr[u].p.num[1])){
		res+=tr[u].p.val;//要不要加上当前点
	}
	res+=qry(tr[u].l,x_1,y_1,x_2,y_2)+qry(tr[u].r,x_1,y_1,x_2,y_2);//递归子树(其实就是小矩形)
	return res;
}
signed main(){
	cin>>n;
	int res=0,op;
	while(cin>>op){
		if(op==1){
			int x,y,v;
			cin>>x>>y>>v;
			x^=res;y^=res;v^=res;
			ins(rt,{x,y,v},0);
		}
		else if(op==2){
			int x_1,y_1,x_2,y_2;
			cin>>x_1>>y_1>>x_2>>y_2;
			x_1^=res;y_1^=res;
			x_2^=res;y_2^=res;
			res=qry(rt,x_1,y_1,x_2,y_2);
			cout<<res<<'\n';
		}
		else break;
	}
	return 0;
}

巧克力王国

还是一道比较板的题。我们考虑把一块巧克力看作平面上的点 \((x,y)\)

然后对于一次询问,我们可以求出 \(a\times x+b\times y\) 的最大和最小值,这个可以根据 \(a,b\) 的正负性分类讨论得出。

然后我们对于巧克力建树。如果发现对于一个人,这个点的子树中的巧克力取到最小都不满足或者取到最大都满足,就可以直接返回。否则递归处理。方式基本和上一题一样,所以直接看代码:

#include<bits/stdc++.h>
#define int long long
#define N 50005
#define alp 0.75
using namespace std;
struct node{
	int num[2],val;
	node(){};
	node(int x,int y,int v){
		num[0]=x;
		num[1]=y;
		val=v;
	}
}p[N];
struct tree{
	int l,r,siz;
	int mx[2],mn[2],sum;
	node p;
}tr[N];
int n,m,cnt,tot,rt,can[N],now;
bool cmpx(node a,node b){
	return a.num[0]<b.num[0];
}
bool cmpy(node a,node b){
	return a.num[1]<b.num[1];
}
int get_new(){
	if(cnt)return can[cnt--];
	return ++tot;
}
void pushup(int u){
	for(int i=0;i<2;i++){
		tr[u].mn[i]=tr[u].mx[i]=tr[u].p.num[i];
		if(tr[u].l){
			tr[u].mn[i]=min(tr[u].mn[i],tr[tr[u].l].mn[i]);
			tr[u].mx[i]=max(tr[u].mx[i],tr[tr[u].l].mx[i]);
		}
		if(tr[u].r){
			tr[u].mn[i]=min(tr[u].mn[i],tr[tr[u].r].mn[i]);
			tr[u].mx[i]=max(tr[u].mx[i],tr[tr[u].r].mx[i]);
		}
	}
	tr[u].sum=tr[tr[u].l].sum+tr[tr[u].r].sum+tr[u].p.val;
	tr[u].siz=tr[tr[u].l].siz+tr[tr[u].r].siz+1;
}
void slap(int u){
	if(!u)return;
	p[++now]=tr[u].p;
	can[++cnt]=u;
	slap(tr[u].l);
	slap(tr[u].r);
}
int rebuild(int l,int r,int d){
	if(l>r)return 0;
	int mid=l+r>>1;
	int u=get_new();
	if(!d)nth_element(p+l,p+mid,p+r+1,cmpx);
	else nth_element(p+l,p+mid,p+r+1,cmpy);
	tr[u].p=p[mid];
	tr[u].l=rebuild(l,mid-1,(d+1)%2);
	tr[u].r=rebuild(mid+1,r,(d+1)%2);
	pushup(u);
	return u;
}
void check(int &u,int d){
	if(tr[tr[u].l].siz>alp*tr[u].siz||tr[tr[u].r].siz>alp*tr[u].siz){
		now=0;
		slap(u);
		u=rebuild(1,tr[u].siz,d);
	}
}
int qry(int u,int a,int b,int c){
	if(!u)return 0;
	int mxx=tr[u].mx[0]*a,mnx=tr[u].mn[0]*a;
	int mxy=tr[u].mx[1]*b,mny=tr[u].mn[1]*b;
	if(a<0)swap(mxx,mnx);
	if(b<0)swap(mxy,mny);
	if(mnx+mny>=c)return 0;//这里就是在用最大值和最小值判断
	if(mxx+mxy<c)return tr[u].sum;
	int res=0;
	if(tr[u].p.num[0]*a+tr[u].p.num[1]*b<c)res+=tr[u].p.val;
	res+=qry(tr[u].l,a,b,c)+qry(tr[u].r,a,b,c);
	return res;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>p[i].num[0]>>p[i].num[1]>>p[i].val;
	}
	rt=rebuild(1,n,0);
	while(m--){
		int a,b,c;
		cin>>a>>b>>c;
		cout<<qry(rt,a,b,c)<<'\n';
	}
	return 0;
}
posted @ 2024-07-16 13:48  zxh923  阅读(1)  评论(0编辑  收藏  举报