K-D Tree
- 数据结构
- K-D Tree
- 简介
- 建树
- 插入/删除
- 重构
- 邻域搜索
- 范围搜索
- 复杂度退化
- 变体
- 练习
- K-D Tree
简介
k-D Tree(KDT , k-Dimension Tree) 是一种可以 高效处理
在结点数
在算法竞赛的题目中,一般有
const double ALPHA = 0.8 , DOUBLE_MAX = numeric_limits<double>::max();;
typedef array<double , K>Point;
Point mn[N] , mx[N] , val[N] , a[N];//以i为根的子树k维坐标最小/最大值;树上结点;原来的点
int rt , tot , lc[N] , rc[N] , cnt[N] , s[N] , sz[N] , sd[N];//树根,节点数,子树大小,左儿子,右儿子
int tc , tmp[N] , *sgt , zero; //重构用数组
double ansmin = DOUBLE_MAX , ansmax = 0;//最近/最远距离
建树
假设我们已经知道了
-
若当前超长方体中只有一个点,返回这个点。
-
选择一个维度,将当前超长方体按照这个维度分成两个超长方体。
-
选择切割点:在选择的维度上选择一个点,这一维度上的值小于这个点的归入一个超长方体(左子树),其余的归入另一个超长方体(右子树)。
-
将选择的点作为这棵子树的根节点,递归对分出的两个超长方体构建左右子树,维护子树的信息。
其构建出 k-D Tree 的形态可能是这样的:
其中树上每个结点上的坐标是选择的分割点的坐标,非叶子结点旁的
这样的复杂度无法保证。对于
-
选择的维度要满足其内部点的分布的差异度最大,即每次选择的切割维度是方差最大的维度。
-
每次在维度上选择切割点时选择该维度上的 中位数,这样可以保证每次分成的左右子树大小尽量相等。
可以发现,使用优化
附:优化
现在,构建 k-D Tree 时间复杂度的瓶颈在于快速选出一个维度上的中位数,并将在该维度上的值小于该中位数的置于中位数的左边,其余置于右边。如果每次都使用 sort 函数对该维度进行排序,时间复杂度是 s[l] 和 s[r] 之间的值按照排序规则 cmp 排序后在 s[mid] 位置上的值,并保证 s[mid] 左边的值小于 s[mid],右边的值大于 s[mid],只需写 nth_element(s+l,s+mid,s+r+1,cmp)。
void Build(int l , int r , int &p , int d)
{
if(l > r)return ;
int mid = (l + r) >> 1;
auto Cmp = [&](const int& x , const int& y)
{
if(val[x][d] != val[y][d])return val[x][d] < val[y][d];
return val[x] < val[y];
};
nth_element(tmp + l , tmp + mid , tmp + r + 1 , Cmp);
p = tmp[mid] , s[p] = sd[p] = 1 , sz[p] = cnt[p] , lc[p] = rc[p] = 0;
mn[p] = mx[p] = val[p];
if(l == r)return ;
Build(l , mid - 1 , lc[p] , (d + 1) % K);
Build(mid + 1 , r , rc[p] , (d + 1) % K);
Pushup(p);
}
重构
如果维护的这个
我们引入一个重构常数
重构时,先遍历子树求出一个序列,然后用以上描述的方法建出一棵 k-D Tree,代替原来不平衡的子树。
bool IsUnbalanced(int p){return cnt[p] && (s[p] * ALPHA <= max(s[lc[p]] , s[rc[p]]) || sd[p] <= s[p] * ALPHA);}
void Travel(int p)
{
if(!p)return ;
Travel(lc[p]);
if(cnt[p])tmp[++tc] = p;
Travel(rc[p]);
}
void Rebuild(int &p)
{
tc = 0;
Travel(p);
Build(1 , tc , p , 0);
}
插入/删除
插入一个
删除操作,则使用 惰性删除,即删除一个结点时打上删除标记,而保留其在 k-D Tree 上的位置。如果这样写,当未删除的结点数在以
void Insert(int &p , const Point& v , int d)
{
if(!p)
{
p = New(v);
return ;
}
if(v[d] < val[p][d])Insert(lc[p] , v , (d + 1) % K);
else if(v == val[p])cnt[p]++;
else Insert(rc[p] , v , (d + 1) % K);
Pushup(p);
if(IsUnbalanced(p))sgt = &p;
}
void Delete(int &p , const Point& v , int d)
{
if(!p)return ;
if(v[d] < val[p][d])Insert(lc[p] , v , (d + 1) % K);
else if(v == val[p])cnt[p]--;
else Insert(rc[p] , v , (d + 1) % K);
Pushup(p);
if(IsUnbalanced(p))sgt = &p;
}
邻域查询
问题:给定
枚举每个结点,对于每个结点找到不等于该结点且距离最小的点,即可求出答案。每次暴力遍历 2-D Tree 上的每个结点的时间复杂度是
此外,还可以使用一种启发式搜索的方法,即若一个结点的两个子树都有可能包含答案,先在与查询点距离最近的一个子树中搜索答案。可以认为,查询点到子树对应的长方形的最近距离就是此题的估价函数。
注意:虽然以上使用的种种优化,但是使用 k-D Tree 单次查询最近点的时间复杂度最坏还是
对于
double Dis(const Point& a , const Point& b)
{
double res = 0;
for(int i = 0 ; i < K ; i++)
res += pow(a[i] - b[i] , 2);
return res;
}
double Fmin(int p , const Point& v)
{
double res = 0;
for(int i = 0 ; i < K ; i++)
res += pow(max(mn[p][i] - v[i] , 0.0) , 2) + pow(max((v[i] - mx[p][i]) , 0.0) , 2);
return res;
}
double Fmax(int p , const Point& v)
{
double res = 0;
for(int i = 0 ; i < K ; i++)
res += pow(max(abs(mx[p][i] - v[i]) , abs(mn[p][i] - v[i])) , 2);
return res;
}
void Qmin(int &p , const Point& v)
{
if(!p)return ;
ansmin = min(ansmin , Dis(val[p] , v));
double disl = Fmin(lc[p] , v) , disr = Fmin(rc[p] , v);
if(disl < ansmin)
{
if(disr < disl)Qmin(rc[p] , v) , disr = ansmin + 1;
if(disl < ansmin)Qmin(lc[p] , v);
}
if(disr < ansmin)Qmin(rc[p] , v);
}
void Qmax(int &p , const Point& v)
{
if(!p)return ;
ansmax = max(ansmax , Dis(val[p] , v));
double disl = Fmax(lc[p] , v) , disr = Fmax(rc[p] , v);
if(disl > ansmax)
{
if(disr > disl)Qmax(rc[p] , v) , disr = ansmax - 1;
if(disl > ansmax)Qmax(lc[p] , v);
}
if(disr > ansmax)Qmax(rc[p] , v);
}
范围搜索
复杂度退化
变体
//TODO K-D Tree 的线段树实现
练习
K-D Tree 基础练习 - Wallace
参考资料
K-D Tree - OI Wiki k-d tree - Wikipedia KDT 小记 - command_block 浅谈偏序问题与K-D Tree - 光与影的彼岸 K-D Tree - Tenshi