Reference: A minimalist’s implementation of an approximate nearest neighbor algorithm in fixed dimensions, Timothy M. Chan
显然的,问题归约到求三维下凸壳。一个静态的三维问题可以被看成一个动态的二维问题,假设每个点 \((x,y,z)\) 在时间 \(t\) 时变为了二维点 \((x,z-ty)\) 。这组(动)二维点的凸包相当于三维凸包与某个动面的交(可能经过缩放变换)。
在 \(t\) 在 \((-\infty,\infty)\) 运动的过程中,我们考虑计算出这个二维凸包的动画,这族二维凸包就可以方便地求出原先的三维凸包。考虑怎么表示这个二维凸包动画:可以发现每个点都是以(不同的)竖直的速度在二维平面上运动,在运动过程中每个点可能进入(成为)下凸壳或离开下凸壳。把他们分别称作“加入事件”和“删除事件”。可以发现每个事件对应于三维凸包上的一个面。(显然地)每个点最多参与每类事件各一次。好了,现在我们知道如何 \(\mathrm O(n)\) 地存储一个下凸壳动画了(一个 \(\mathrm O(n)\) 的初始状态,\(\mathrm O(n)\) 个按时间排好序的事件)。
接下来考虑怎么求这个动画。我们对 \(x\) 轴排序、分治(注意 \(x\) 是不动的)。 \(n\leq 2\) 的情形是显然的。假设左半边和右半边做好了,考虑两边的合并。首先静态二维情况下,\(x\)轴分离的两个点集 \(L,R\) 的并的凸壳是它们的凸壳,加上两个凸壳的公切线(设 \(u,v\) 分别为其切点),再去掉公切线之上的部分。
我们用 \(x^-\) 表示凸壳上 \(x\) 左侧的邻居, \(x^+\) 表示其右侧的邻居。
动态情况下,我们只需要维护那条公切线和与它相关的事件就好了。我们模拟时间的进行,依次处理每个事件。这里有若干种待选的发生事件,我们分别计算出这几个事件哪个最先发生,然后将时间快进到这个事件发生的时候处理这一事件就好了。可能的待选事件有:
- \(u\) 的左侧,\(L\) 上发生了一个插入或删除事件。
- \(v\) 的右侧,\(R\) 上发生了一个插入或删除事件。
- \(u^-\) 从上到下运动穿过了 \(uv\) 的延长线,此时 \(u\) 被删除,\(u^-v\) 成为新切线。
- \(v^+\) 从上到下运动穿过了 \(uv\) 的延长线,同理。
- \(u^+\) 从上到下运动穿过了 \(uv\) ,此时 \(u^+\) 被加入,\(u^+v\) 成为新切线。
- \(v^-\) 从上到下运动穿过了 \(uv\) ,同理。
可以发现将两个大小相近的凸壳动画合并的复杂度是 \(\mathrm O(n)\) 的,因此分治的总复杂度是 \(\mathrm O(n\log{n})\) 的。
以下这一份是论文原作者提供的代码,不到90个非空行,这份实现将空间卡到了 \(6n\) 个指针,实际上(在稍微损失空间的情况下)代码应当可以更简洁。
// Timothy Chan "ch3d.cc" 12/02 3-d lower hull (in C++)
// a simple implementation of the O(n log n) divide-and-conquer algorithm
// input: coordinates of points
// n x_0 y_0 z_0 ... x_{n-1} y_{n-1} z_{n-1}
// output: indices of facets
// i_1 j_1 k_1 i_2 j_2 k_2 ...
// warning: ignores degeneracies and robustness
// space: uses 6n pointers
#include <iostream>
struct Point {
double x, y, z;
*prev, *next;
Point void act() {
if (prev->next != this) prev->next = next->prev = this; // insert
else { prev->next = next; next->prev = prev; } // delete
}
};
const double INF = 1e99;
static Point nil = {INF, INF, INF, 0, 0};
*NIL = &nil;
Point
inline double turn(Point *p, Point *q, Point *r) { // <0 iff cw
if (p == NIL || q == NIL || r == NIL) return 1.0;
return (q->x-p->x)*(r->y-p->y) - (r->x-p->x)*(q->y-p->y);
}
inline double time(Point *p, Point *q, Point *r) {
if (p == NIL || q == NIL || r == NIL) return INF;
return ((q->x-p->x)*(r->z-p->z) - (r->x-p->x)*(q->z-p->z)) / turn(p,q,r);
}
*sort(Point P[], int n) { // mergesort
Point *a, *b, *c, head;
Point
if (n == 1) { P[0].next = NIL; return P; }
= sort(P, n/2);
a = sort(P+n/2, n-n/2);
b = &head;
c do
if (a->x < b->x) { c = c->next = a; a = a->next; }
else { c = c->next = b; b = b->next; }
while (c != NIL);
return head.next;
}
void hull(Point *list, int n, Point **A, Point **B) { // main algorithm
*u, *v, *mid;
Point double t[6], oldt, newt;
int i, j, k, l, minl;
if (n == 1) { A[0] = list->prev = list->next = NIL; return; }
for (u = list, i = 0; i < n/2-1; u = u->next, i++) ;
= v = u->next;
mid (list, n/2, B, A); // recurse on left and right sides
hull(mid, n-n/2, B+n/2*2, A+n/2*2);
hull
for ( ; ; ) // find initial bridge
if (turn(u, v, v->next) < 0) v = v->next;
else if (turn(u->prev, u, v) < 0) u = u->prev;
else break;
// merge by tracking bridge uv over time
for (i = k = 0, j = n/2*2, oldt = -INF; ; oldt = newt) {
[0] = time(B[i]->prev, B[i], B[i]->next);
t[1] = time(B[j]->prev, B[j], B[j]->next);
t[2] = time(u, u->next, v);
t[3] = time(u->prev, u, v);
t[4] = time(u, v->prev, v);
t[5] = time(u, v, v->next);
tfor (newt = INF, l = 0; l < 6; l++)
if (t[l] > oldt && t[l] < newt) { minl = l; newt = t[l]; }
if (newt == INF) break;
switch (minl) {
case 0: if (B[i]->x < u->x) A[k++] = B[i]; B[i++]->act(); break;
case 1: if (B[j]->x > v->x) A[k++] = B[j]; B[j++]->act(); break;
case 2: A[k++] = u = u->next; break;
case 3: A[k++] = u; u = u->prev; break;
case 4: A[k++] = v = v->prev; break;
case 5: A[k++] = v; v = v->next; break;
}
}
[k] = NIL;
A
->next = v; v->prev = u; // now go back in time to update pointers
ufor (k--; k >= 0; k--)
if (A[k]->x <= u->x || A[k]->x >= v->x) {
[k]->act();
Aif (A[k] == u) u = u->prev; else if (A[k] == v) v = v->next;
}
else {
->next = A[k]; A[k]->prev = u; v->prev = A[k]; A[k]->next = v;
uif (A[k]->x < mid->x) u = A[k]; else v = A[k];
}
}
int main() {
int n, i;
>> n;
cin
*P = new Point[n]; // input
Point for (i = 0; i < n; i++) { cin >> P[i].x; cin >> P[i].y; cin >> P[i].z; }
*list = sort(P, n);
Point **A = new Point *[2*n], **B = new Point *[2*n];
Point (list, n, A, B);
hull
for (i = 0; A[i] != NIL; A[i++]->act()) // output
<< A[i]->prev-P << " " << A[i]-P << " " << A[i]->next-P << "\n";
cout delete A; delete B; delete P;
}