【科技】KD-tree随想
大概就是个复杂度对的暴力做法,在你不想写二维线段树等的时候优秀的替代品。
优点:思路简单,代码好写。
他大概有两种用法(虽然差不多)。
在平面坐标系中干一些事情:
例如最常规的平面最近最远点,不管是欧几里得距离还是曼哈顿距离,本质上都是一样的。
利用不同维度的尽量平均的分割,再在询问时剪枝。
这里给出一个曼哈顿距离上的最近最远距离的版本,可供参考:
1 namespace KD { 2 int Rt, lc[N], rc[N], u[N], d[N], l[N], r[N]; 3 inline void Merge(int x, int y) { 4 u[x] = std::max(u[x], u[y]); 5 d[x] = std::min(d[x], d[y]); 6 l[x] = std::min(l[x], l[y]); 7 r[x] = std::max(r[x], r[y]); 8 } 9 inline void Up(int t) { 10 l[t] = r[t] = p[t].v[0]; 11 u[t] = d[t] = p[t].v[1]; 12 if (lc[t]) Merge(t, lc[t]); 13 if (rc[t]) Merge(t, rc[t]); 14 } 15 int Build(int l, int r, int dep) { 16 if (l >= r) { 17 if (l == r) Up(l); 18 return (l == r)? (l) : (0); 19 } 20 Mt = dep & 1; int md = (l + r) >> 1; 21 std::nth_element(p + l, p + md, p + 1 + r); 22 lc[md] = Build(l, md - 1, dep + 1); 23 rc[md] = Build(md + 1, r, dep + 1); 24 Up(md); return md; 25 } 26 inline int In_mi(int t) { 27 int re = 0; 28 re += std::max(qi.v[0] - r[t], 0); 29 re += std::max(l[t] - qi.v[0], 0); 30 re += std::max(qi.v[1] - u[t], 0); 31 re += std::max(d[t] - qi.v[1], 0); 32 return re; 33 } 34 inline int In_ma(int t) { 35 int re = 0; 36 re += std::max(std::abs(qi.v[0] - r[t]), std::abs(qi.v[0] - l[t])); 37 re += std::max(std::abs(qi.v[1] - u[t]), std::abs(qi.v[1] - d[t])); 38 return re; 39 } 40 void Query_mi(int t, int dep) { 41 if (!t) return; Mt = dep & 1; 42 if (qi != p[t]) ani = std::min(ani, Dis(qi, p[t])); 43 int dl = (lc[t])? (In_mi(lc[t])) : (INF); 44 int dr = (rc[t])? (In_mi(rc[t])) : (INF); 45 if (dl < dr) { 46 if (ani > dl) Query_mi(lc[t], dep + 1); 47 if (ani > dr) Query_mi(rc[t], dep + 1); 48 } else { 49 if (ani > dr) Query_mi(rc[t], dep + 1); 50 if (ani > dl) Query_mi(lc[t], dep + 1); 51 } 52 } 53 void Query_ma(int t, int dep) { 54 if (!t) return; Mt = dep & 1; 55 ana = std::max(ana, Dis(qi, p[t])); 56 int dl = (lc[t])? (In_ma(lc[t])) : (0); 57 int dr = (rc[t])? (In_ma(rc[t])) : (0); 58 if (dl > dr) { 59 if (ana < dl) Query_ma(lc[t], dep + 1); 60 if (ana < dr) Query_ma(rc[t], dep + 1); 61 } else { 62 if (ana < dr) Query_ma(rc[t], dep + 1); 63 if (ana < dl) Query_ma(lc[t], dep + 1); 64 } 65 } 66 }
通常带有表示点的结构体:
1 struct No { 2 int v[2]; 3 inline void Read() { 4 scanf("%d%d", &v[0], &v[1]); 5 } 6 inline friend bool operator < (No a, No b) { 7 return a.v[Mt] < b.v[Mt]; 8 } 9 } p[N], qi;
(注:$qi$表示当前询问点,$Mt$表示当前分割的维度)
当然还有某些问题要求第$k$远点,只要每次查到一个点就扔到堆里去,时时维护最远的$k$个就好了,因为KD-tree剪掉了很多不必要的点,所以可以认为扔到堆里的元素并不多。
或者说动态的问题需要动态开点,开多了就可能导致树不平衡,隔一会重构就好了。
当然KD-tree在坐标系上最大的优越之处在于乱搞,旋转一下坐标系之后什么都拦不住KD-tree啦。
比如说APIO 2018的选圈圈。。。把圆用矩形框起来,每次暴力找就好了。
1 #include <cstdio> 2 #include <algorithm> 3 4 const int N = 300005; 5 const double Alpha = 1.926, EPS = 1e-4; 6 7 int n, Mt, ans[N]; 8 9 struct No { 10 double v[2], r; int id; 11 inline void Read(double x = 0, double y = 0) { 12 scanf("%lf%lf%lf", &x, &y, &r); 13 v[0] = x * cos(Alpha) + y * sin(Alpha); 14 v[1] = y * cos(Alpha) - x * sin(Alpha); 15 } 16 inline friend bool operator < (No a, No b) { 17 return a.v[Mt] < b.v[Mt]; 18 } 19 } pp[N], p[N], qi; 20 21 inline bool cmp_r(No a, No b) { 22 return (a.r == b.r)? (a.id < b.id) : (a.r > b.r); 23 } 24 inline double Sqr(double x) { 25 return x * x; 26 } 27 28 namespace KD { 29 int Rt, lc[N], rc[N]; 30 double l[N], r[N], d[N], u[N]; 31 inline void Merge(int x, int y) { 32 l[x] = std::min(l[x], l[y]); 33 r[x] = std::max(r[x], r[y]); 34 d[x] = std::min(d[x], d[y]); 35 u[x] = std::max(u[x], u[y]); 36 } 37 inline void Up(int t) { 38 l[t] = p[t].v[0] - p[t].r; 39 r[t] = p[t].v[0] + p[t].r; 40 d[t] = p[t].v[1] - p[t].r; 41 u[t] = p[t].v[1] + p[t].r; 42 if (lc[t]) Merge(t, lc[t]); 43 if (rc[t]) Merge(t, rc[t]); 44 } 45 int Build(int l, int r, int dep) { 46 if (l >= r) return (l == r)? (Up(l), l) : (0); 47 Mt = dep & 1; int md = (l + r) >> 1; 48 std::nth_element(p + l, p + md, p + 1 + r); 49 lc[md] = Build(l, md - 1, dep + 1); 50 rc[md] = Build(md + 1, r, dep + 1); 51 Up(md); return md; 52 } 53 inline int Out(int t) { 54 int re1 = r[t] < qi.v[0] - qi.r - EPS || l[t] > qi.v[0] + qi.r + EPS; 55 int re2 = u[t] < qi.v[1] - qi.r - EPS || d[t] > qi.v[1] + qi.r + EPS; 56 return re1 || re2; 57 } 58 inline int Check(int t) { 59 return Sqr(p[t].r + qi.r) + EPS >= Sqr(p[t].v[0] - qi.v[0]) + Sqr(p[t].v[1] - qi.v[1]); 60 } 61 void Query(int t) { 62 if (!t || Out(t)) return; 63 if (!ans[p[t].id] && Check(t)) ans[p[t].id] = qi.id; 64 if (lc[t]) Query(lc[t]); 65 if (rc[t]) Query(rc[t]); 66 } 67 } 68 69 int main() { 70 scanf("%d", &n); 71 for (int i = 1; i <= n; ++i) { 72 p[i].Read(); 73 p[i].id = i; 74 pp[i] = p[i]; 75 } 76 std::sort(pp + 1, pp + 1 + n, cmp_r); 77 KD::Rt = KD::Build(1, n, 0); 78 79 for (int i = 1; i <= n; ++i) { 80 if (!ans[pp[i].id]) { 81 ans[pp[i].id] = pp[i].id; 82 qi = pp[i]; 83 KD::Query(KD::Rt); 84 } 85 } 86 for (int i = 1; i <= n; ++i) { 87 printf("%d ", ans[i]); 88 } 89 90 return 0; 91 }
二维线段树的替代品:
由于KD-tree本身就和值域没有什么关系,涉及到二维数点、矩形修改、矩形询问等问题可以比较方便的做,只要每个点维护一个矩形,然后大致就和线段树差不多了。
其实很多问题都能转化为二维平面甚至多维上的数点问题,有些问题离线后把时间也算成一维也是一个常用套路,KD-tree在这方面处理能力较强,适用范围较广。
要注意KD-tree上每一个点都是一个真实的点,修改时不要忘记更新它本身。
可能左右两个子节点表示的矩形存在相交,有时候自顶向下不一定好。