我希望创建一个2D平面内的大量随机点云,这些点不能退化(集合中不存在三个共线点)。我有一个天真的解决方案,它生成一个随机的浮点数对P_new(x,y),并检查直到现在生成的每一对点(P1,P2),看看点(P1,P2,P)是否在同一条直线上。这需要对每个新增加到列表中的点进行O(n^2)次检查,使得整个复杂度为O(n^3),如果我想生成超过4000个点,就会非常慢(需要超过40分钟)。是否有更快的方法来生成这些非退化点集?
我希望创建一个2D平面内的大量随机点云,这些点不能退化(集合中不存在三个共线点)。我有一个天真的解决方案,它生成一个随机的浮点数对P_new(x,y),并检查直到现在生成的每一对点(P1,P2),看看点(P1,P2,P)是否在同一条直线上。这需要对每个新增加到列表中的点进行O(n^2)次检查,使得整个复杂度为O(n^3),如果我想生成超过4000个点,就会非常慢(需要超过40分钟)。是否有更快的方法来生成这些非退化点集?
不必在每个循环迭代中检查可能的点共线性,而是可以计算并比较线性方程的系数。这些系数应存储在具有快速搜索功能的容器中。我考虑使用std::set,但unordered_map也可以适用且可能导致更好的结果。
总之,我建议采用以下算法:
p
;p
和现有点的直线的系数(我的意思是通常的 A
、B
和 C
)。在这里,你需要进行 n
次计算;n*log(n^2)
个操作。O(log(n))
。整个复杂度降至 O(n^2*log(n))
。
该算法需要额外存储 n^2*sizeof(Coefficient)
的内存。但如果只计算4000个点,这似乎是可以接受的。
Ax+By+C=0
,而不是kx+b=0
。竖线方程是Ax+C=0
(B=0
),这里没有无限数量。 - eugene_chen ^ 2
个元素。因此,每个模式的二分搜索复杂度为log(n * n)
。有n
个模式。因此,总体复杂度为O(n * log(n))
。 - eugene_che可以通过以下方式轻松构建O(n^2 log n)算法:
对于集合中的每个点P:
Sort other points by polar angle (cross-product as a comparison function, standard idea, see 2D convex hull gift-wrapping algorithm for example). In this step you should consider only points Q that satisfy
Q.x > P.x || Q.y >= P.y
Iterate over sorted list, equal points are lying on the same line.
排序的时间复杂度为O(n log n),第二步是O(n)。这将使去除退化点的时间复杂度为O(n^2 log n)。
生成随机点Q。
对于之前的点P,计算(dx, dy) = P - Q
。
并且B = (abs(dx) > abs(dy) ? dy/dx : dx/dy)
。
按照其B值对点P的列表进行排序,以便与Q形成线条的点在排序后的列表中靠近位置。
遍历排序后的列表,检查Q与当前考虑的P值以及一些比给定距离更近的下一个值形成线条的位置。
Perl实现:
#!/usr/bin/perl
use strict;
use warnings;
use 5.010;
use Math::Vector::Real;
use Math::Vector::Real::Random;
use Sort::Key::Radix qw(nkeysort);
use constant PI => 3.14159265358979323846264338327950288419716939937510;
@ARGV <= 2 or die "Usage:\n $0 [n_points [tolerance]]\n\n";
my $n_points = shift // 4000;
my $tolerance = shift // 0.01;
$tolerance = $tolerance * PI / 180;
my $tolerance_arctan = 3 / 2 * $tolerance;
# I got to that relation using no so basic maths in a hurry.
# it may be wrong!
my $tolerance_sin2 = sin($tolerance) ** 2;
sub cross2d {
my ($p0, $p1) = @_;
$p0->[0] * $p1->[1] - $p1->[0] * $p0->[1];
}
sub line_p {
my ($p0, $p1, $p2) = @_;
my $a0 = $p0->abs2 || return 1;
my $a1 = $p1->abs2 || return 1;
my $a2 = $p2->abs2 || return 1;
my $cr01 = cross2d($p0, $p1);
my $cr12 = cross2d($p1, $p2);
my $cr20 = cross2d($p2, $p0);
$cr01 * $cr01 / ($a0 * $a1) < $tolerance_sin2 or return;
$cr12 * $cr12 / ($a1 * $a2) < $tolerance_sin2 or return;
$cr20 * $cr20 / ($a2 * $a0) < $tolerance_sin2 or return;
return 1;
}
my ($c, $f1, $f2, $f3) = (0, 1, 1, 1);
my @p;
GEN: for (1..$n_points) {
my $q = Math::Vector::Real->random_normal(2);
$c++;
$f1 += @p;
my @B = map {
my ($dx, $dy) = @{$_ - $q};
abs($dy) > abs($dx) ? $dx / $dy : $dy / $dx;
} @p;
my @six = nkeysort { $B[$_] } 0..$#B;
for my $i (0..$#six) {
my $B0 = $B[$six[$i]];
my $pi = $p[$six[$i]];
for my $j ($i + 1..$#six) {
last if $B[$six[$j]] - $B0 > $tolerance_arctan;
$f2++;
my $pj = $p[$six[$j]];
if (line_p($q - $pi, $q - $pj, $pi - $pj)) {
$f3++;
say "BAD: $q $pi-$pj";
redo GEN;
}
}
}
push @p, $q;
say "GOOD: $q";
my $good = @p;
my $ratiogood = $good/$c;
my $ratio12 = $f2/$f1;
my $ratio23 = $f3/$f2;
print STDERR "gen: $c, good: $good, good/gen: $ratiogood, f2/f1: $ratio12, f3/f2: $ratio23 \r";
}
print STDERR "\n";
公差指当考虑三个点是否在一条直线上时的可接受误差,其表示为π-max_angle(Q, Pi, Pj)
中的角度。
它并没有考虑到在减去向量时可能出现的数值不稳定性(即|Pi-Pj|
可能比|Pi|
小几个数量级)。消除该问题的简单方法是还要要求任意两个给定点之间的最小距离。
将公差设置为1e-6,程序只需几秒钟即可生成4000个点。将其转换为C/C++可能会使其快两个数量级。
O(n)解决方案:
P(cos(2 × π × r), sin(2 × π × r))