loading

KDT 瞎说

KDT 适合处理一些二维及更高维度的问题,典型的板子是 P4148:给定一张二维平面,支持插入一个点,或者查询矩形内权值和,强制在线,20M 空间。如果没有后两个限制,离线可以简单的使用 cdq 分治,在线且空限足够可以树套树。有了后两个限制就只能分块或 KDT 了。

KDT 是一棵二叉搜索树(不过维护起来跟线段树很像而不是平衡树),每一个节点维护平面上的一个点,一棵子树代表了一个矩形范围内的所有点。由于是二叉搜索树,所以肯定存在某一维使得一个点的左儿子在这一维上的坐标小于该点,且右儿子大于该点。实际运用中为了保证树高,对于每一层满足性质的维度是交替的。

由于 OI 中常见的是二维的,所以以下都以 2DT 为例分析。

建树

要求:给定若干个点,要求对这些点建 KDT。

为了让树高平衡,假设我们在这一层处理的是第 \(t\) 维,那么在这一维上我们肯定希望让两边点数尽可能平均。那么我们选出这一维上的中位数为根节点即可,然后让这一维上比它小的去左边,比它大的去右边。实现上,把中点 \(mid=\lfloor\frac{l+r}{2}\rfloor\) 取出来,使用 nth_element 找出第 \(mid\) 大的然后让 \([l,mid)\) 递归左边 \((mid,r]\) 递归右边即可。由此可见,这一维上和根相等的点分到哪边都有可能。别忘了递归下一层时要变 \(t\)

这么做的树高是 \(O(\log n)\) 的(准确来说是 \(\log n+O(1)\),这是 KDT 复杂度证明的重要前提),而 nth_element 是线性的,那么每一层做这个东西也就是线性的,所以总复杂度是 \(O(n\log n)\)

每个节点内通常要维护节点的坐标、节点子树代表的矩形的在每一维的边界。为了方便维护,我们对每一维都开一个数组维护这些东西。

查询

要求:给定一个矩形,求矩形内点的权值和。

假设我们要处理 \(p\) 节点所属子树代表的矩形对查询矩形的贡献。那么,如果查询矩形包含 \(p\) 子树矩形,直接返回 \(p\) 子树内节点权值和即可。如果查询矩形与子树矩形无交,那么返回 0。剩余情况,首先计算 \(p\) 代表的那个点对查询矩形的贡献,然后暴力递归进左右儿子。

可以证明,此时时间复杂度是 \(O(\sqrt n)\)。在 k 维情况下,复杂度是 \(O(n^{1-\frac{1}{k}})\)

同时,能做矩形查询的同时当然也可以做矩形修改,此时需要多维护懒标记,和线段树差不多。

插入

很遗憾,朴素 KDT 并不支持动态插入一个点这个东西。但是我们可以通过其他手段支持动态插入(静态的话直接提前建好树然后打个标记表示该点存在性就行)。

二进制分组

较为常用。

我们维护若干棵大小为 2 的次幂的 KDT,初始我们有 1 个点待插入,从低位向高位遍历,假设当前有 \(2^i\) 个点待插入,那么如果此时存在一棵大小为 \(2^i\) 的树,那么暴力遍历这棵树将树解构,然后将树内的点加入待插入点中,这样就有了 \(2^{i+1}\) 个点待插入,直到不存在这样的树为止,此时按照建树方法将待插入点的树建出来即可。

显然每个点只会被重构 \(O(\log n)\) 次,所以总复杂度 \(O(n\log^2n)\)。什么矩形修改查询也得对 \(O(\log n)\) 棵树做,总复杂度没变。

根号重构

设一个阈值 \(B\),我们先存下来最近 \(B\) 次插入的点(矩形修改查询暴力遍历这些点即可),每插入 \(B\) 次就重构一次 KDT,这样做插入复杂度是 \(O(\frac{n^2\log n}{B})\),查询复杂度是 \(O(n\sqrt n+B)\),取 \(B=\sqrt{n\log n}\) 复杂度就是 \(O(n\sqrt n+n\sqrt{n\log n})\),k 维下是 \(O(n^{1-\frac{1}{k}}+n\sqrt{n\log n})\),复杂度并不比二进制分组优。

例题代码

int op;
struct node{
	//节点子树的矩形范围 
	int l[2],r[2];
	//节点坐标 
	int x[2],num,ans;
	int ch[2];
}d[maxn];
using ai3=array<int,3>;
ai3 arr[maxn];
int cnt;
bool xmp(ai3 x,ai3 y){return x[0]<y[0];}
bool ymp(ai3 x,ai3 y){return x[1]<y[1];}
queue<int>q;
void destruct(int &p){
	if(!p)return;
	q.push(p),arr[++cnt]={d[p].x[0],d[p].x[1],d[p].num};
	int ls=d[p].ch[0],rs=d[p].ch[1];
	rep(i,0,1)d[p].l[i]=d[p].r[i]=d[p].x[i]=d[p].ch[i]=0;
	d[p].num=d[p].ans=0,p=0;
	destruct(ls),destruct(rs);
}
const int m=17;
int rt[20];
il void pu(int p){
	rep(i,0,1)if(d[p].ch[i]){
		rep(j,0,1)ckmn(d[p].l[j],d[d[p].ch[i]].l[j]),ckmx(d[p].r[j],d[d[p].ch[i]].r[j]);
		d[p].ans+=d[d[p].ch[i]].ans;
	}
}
void bd(int t,int l,int r,int &p){
	if(l>r)return;
	p=q.front(),q.pop();
	int mid=(l+r)>>1;
	nth_element(arr+l,arr+mid,arr+r+1,t?xmp:ymp);
	rep(i,0,1)d[p].x[i]=d[p].l[i]=d[p].r[i]=arr[mid][i];
	d[p].num=d[p].ans=arr[mid][2];
	bd(t^1,l,mid-1,d[p].ch[0]),bd(t^1,mid+1,r,d[p].ch[1]);
	pu(p);
}
int ql[2],qr[2];
int qry(int p){
	if(!p)return 0;
	rep(i,0,1)if(d[p].r[i]<ql[i]||qr[i]<d[p].l[i])return 0;
	bool ok=1;
	rep(i,0,1)ok&=(ql[i]<=d[p].l[i]&&d[p].r[i]<=qr[i]);
	if(ok)return d[p].ans;
	int ans=0;
	ok=1;
	rep(i,0,1)ok&=(ql[i]<=d[p].x[i]&&d[p].x[i]<=qr[i]);
	if(ok)ans=d[p].num;
	return ans+qry(d[p].ch[0])+qry(d[p].ch[1]);
}
inline void solve_the_problem(){
	rd();
	rep(i,1,200000)q.push(i);
	int lstans=0;
	while((op=rd())!=3){
		if(op==1){
			int x=rd()^lstans,y=rd()^lstans,z=rd()^lstans;
			arr[cnt=1]={x,y,z};
			rep(i,0,m){
				if(!rt[i]){bd(1,1,cnt,rt[i]);break;}
				else destruct(rt[i]);
			}
		}else{
			ql[0]=rd()^lstans,ql[1]=rd()^lstans,qr[0]=rd()^lstans,qr[1]=rd()^lstans;
			int ans=0;
			rep(i,0,m)ans+=qry(rt[i]);
			write(lstans=ans);
		}
	}
}
posted @ 2025-09-01 16:55  dcytrl  阅读(32)  评论(2)    收藏  举报