$O(n^2)$ 的 Voronoi Diagram (V 图)简单算法
本来是想 WF 之后再写的,但是 WF 推迟了,也就先写了吧。
我们知道,我们其实是可以用半平面交来求 V 图的,就是每个点和其他所有点中垂线半平面的交,时间复杂度是 \(n\) 次半平面交,\(O(n^2\log_2 n)\)。
但是,我们其实可以暴力做半平面交,维护已经考虑的半平面交出来的凸包,然后加入新的半平面,以 \(O(凸包大小)\) 的时间更新。
如果我们在 shuffle 半平面后再干这件事情,其实时间复杂度就是期望 \(O(n^2)\) 了,实际效率比暴力半平面交类似或者更快。
对于最远点的 V 图,我们一样的做法时间复杂度也是 \(O(n^2)\)。
附一份代码,【WF2018】 熊猫保护区:
std::vector<line> cut(const std::vector<line> & o, line l) {
std::vector<line> res;
int n = size(o);
for(int i = 0;i < n;++i) {
line a = o[i], b = o[(i + 1) % n], c = o[(i + 2) % n];
int va = check(a, b, l), vb = check(b, c, l);
if(va > 0 || vb > 0 || (va == 0 && vb == 0)) {
res.push_back(b);
}
if(va >= 0 && vb < 0) {
res.push_back(l);
}
}
return res;
}
std::vector<std::vector<line>> voronoi(std::vector<vec2> p) {
int n = p.size();
auto b = p; shuffle(b.begin(), b.end(), gen);
const db V = 3e4;
std::vector<std::vector<line>> a(n, {
{V, 0, V * V}, {0, V, V * V},
{-V, 0, V * V}, {0, -V, V * V},
});
for(int i = 0;i < n;++i) {
for(vec2 x : b) if((x - p[i]).abs() > eps) {
a[i] = cut(a[i], bisector(p[i], x));
}
}
return a;
}
时间复杂度证明
记 $f(x, S) $ 为,$x$ 与 $S-\{x\}$ 中的点中垂线半平面交的凸包大小。n 个点的 V 图有 \(O(n)\) 个边界,故 \(\sum_{x\in S} f(x, S) = O(|S|)\)
假设 \(p\) 是所有点的均匀随机排列:
\[\begin{aligned}
E[\sum_{i=1}^n \sum_{j=1}^n f(p[i], p[1\cdots j])]&= \sum_{j=1}^n \sum_{i=1}^n E[f(p[i], p[1\cdots j])] \\
&= \sum_{j=1}^n (O(j)+\sum_{i=j+1}^n E[f(p[i], p[1\cdots j])]) \\
&= O(n^2)
\end{aligned}
\]
最后一步懒得写了,大概你可以认为先 sample 出了 \(\{p[i]\} \cup p[1\cdots j]\) 整个集合,再 sample 出了 \(p[i]\),所以期望是 \(O(1)\) 的。
方差太难了,不会分析。
特别需要注意的是,我们并没有分析出来对于一个 \(x\),\(\sum_{j=1}^n f(x, p[1\cdots j])\) 是期望 \(O(n)\) 的,事实上这也是错的。