分治法求解最近点对问题
一、实验目的与要求
1、实验基本要求
(1) 掌握分治法思想。
(2) 学会最近点对问题求解方法。
2、实验亮点
(1)通过暴力法以及分治法详细介绍“最近点对”问题的实现方法
(2)完成两种算法的图形界面可视化,使算法执行过程更清晰
(3)完成数据运行效率的对比
(4)自行绘制了算法示意图和算法流程图,实验展示更清晰
(5)完成了分治法在三维下的推广
二、实验内容与方法
- 对于平面上给定的N个点,给出所有点对的最短距离,即,输入是平面上的N个点,输出是N点中具有最短距离的两点。
- 要求随机生成N个点的平面坐标,应用蛮力法编程计算出所有点对的最短距离。
- 要求随机生成N个点的平面坐标,应用分治法编程计算出所有点对的最短距离。
- 分别对N=100,1000,10000,100000,统计算法运行时间,比较理论效率与实测效率的差异,同时对蛮力法和分治法的算法效率进行分析和比较。
- 如果能将算法执行过程利用图形界面输出,可获加分。
三、实验步骤与过程
(一)一些准备工作
1、实验流程
通过随机数生成器生成double型点集数据。然后对点对集合分别使用暴力穷举法与分治法进行运算,编程计算出所有点对的最短距离及运行时间,最后进行算法效率分析与对比得出实验结论。
2、数据生成与去除重复点
可以通过C++自带函数生成随机值。但随机值生成过程中有可能存在生成重复点的情况,因此需要对重复的点进行剔除。
在此我使用unordered_set结构并利用哈希完成对重复点的剔除。
①首先定义点结构体:
//定义点结构体
struct Point {
double x, y;
};
②重载比较函数与哈希函数
//重载比较函数
struct eqfunc {
bool operator()(const Point &p1, const Point &p2) const {
return ((p1.x == p2.x) && (p1.y == p2.y));
}
};
//重载哈希函数
struct hashfunc {
size_t operator()(const Point &P) const {
return size_t(P.x * 201928 + P.y);
}
};
③使用函数生成数据并利用哈希对重复点进行剔除
//利用哈希函数进行去重
unordered_set<Point, hashfunc, eqfunc> hash;
//遍历赋值并哈希去重
for (int i = 0; i < length; i++) {
point[i].x = dist(eng);
point[i].y = dist(eng);
if (hash.find(point[i]) == hash.end()) {
hash.insert(point[i]);
}else {
i--;
}
}
对于每个生成的随机数,分别赋值给横纵坐标,再利用哈希函数,如果在集合中找不到,则说明点未重复,则进行下一个点的生成。如果找到点,则将计数器自减1,并重新生成该点的横纵坐标值。
(二)暴力穷举法
1、算法描述
暴力穷举法即遍历求出每个点之间的距离,并选择距离最小的点进行输出。
具体流程图如下:
2、时间复杂度分析
不妨将问题看成一个求n边无向完全图的问题。则对于 n n n个顶点有每个顶点存在连边 n − 1 n-1 n−1条,又因为是无向图,则共有连边 C = n ( n − 1 ) 2 C=\frac{n\left(n-1\right)}{2} C=2n(n−1)。因此时间复杂度为 O ( n 2 ) O\left(n^2\right) O(n2)
3、编程实现
double ans = INF; //设定初始结果为无穷大
for (int i = 0; i <= length - 1; i++)
{
for (int j = i + 1; j <= length - 1; j++)
{
//遍历每一组可能,并选择最小值存入结果
ans = min(ans, dis(point[i], point[j]));
}
}
设置初始结果为INF。通过外层的循环遍历各点,再内层的循环遍历还未与当前点比较过的点,当发现存在两点间距离小于目前结果时,则更新结果值为更小的距离,依次循环穷举所有点对间距离。
(三)分治法
1、算法描述
基于暴力穷举法,我们在实际运算中不难发现,存在一些“显然距离很远”的点对,对这些点对的距离运算并进行比较常常是一种无用功,将造成时间空间的双重浪费。因此能否找到一种方法精简暴力穷举法,降低无用比较次数呢?
与排序算法类似,完成实验一后,不难发现:对于排序算法,快速排序与归并排序与其余三个排序算法比都是比较省时间的。这说明,将一个大整体分为几个小部分进行“分而治之”是解决类似问题的好方法。因此,相似地,我们可以使用分治法,利用“分而治之”的思想对问题实现简化。依据分治法,我们可以设计出如下算法:
①对所有点按照其横坐标进行排序;
②若
n
=
1
n=1
n=1,返回无穷大;
n
=
2
n=2
n=2,计算距离并返回;
n
>
2
n>2
n>2,以该组数组下标中值分割,即mid=(l+r)/2,mid_x=point[mid].x,将规模为n的问题分解为两个规模为
n
2
\frac{n}{2}
2n的子问题并重复步骤②,直到得到两个子问题的解 mindis1,mindis2;
③合并子问题的点并将其按y坐标排序;
④设mindis为两个子问题解中的较小值,遍历当前规模下所有点,找出横坐标范围在[mid_x-mindis,mid_x+mindis]的所有点;
⑤将步骤④中的所有点与按y坐标排好序后的其后6个基准点(具体算法证明将在下面完成)进行比较,如果距离小于 mindis 则更新 mindis 的值。
具体流程图如下:
2、算法解释与正确性分析
(1)点集分块:
我们不妨将平面分为两个大致相等的点集再对问题进行考虑。
将平面的点分为两个点集,此时对于某点对中两个点
p
1
p_1
p1,
p
2
p_2
p2存在如下三种情况:
①
p
1
,
p
2
p_1,p_2
p1,p2都位于左侧点集
②
p
1
,
p
2
p_1,p_2
p1,p2都位于右侧点集
③
p
1
,
p
2
p_1,p_2
p1,p2一个位于左侧点集,另一个位于右侧点集
对于情况①与②,可以通过递归进行解决,难点在于如何处理两个点不处于同一个点集的情况。
(2)鸽笼查找
不妨设左右两侧点集的最小距离为
d
l
d_l
dl,
d
r
d_r
dr。因此有如下结论:
左、右侧点集中每个点对间距离均分别大于等于
d
l
d_l
dl,
d
r
d_r
dr
因此,仅需对下图中蓝色方框内的异侧点对进行检测即可:
其中,
d
=
min
(
d
l
,
d
r
)
d=\min{\left(d_l,d_r\right)}
d=min(dl,dr)。
对于蓝色区域内的点,如果仍然采用暴力穷举法对每个点对进行检测,不能实质性降低时间复杂度以节省时间。因此,我们可以利用鸽笼原理,找到对应的基准点进行检测。
如下图,当对
[
l
−
δ
,
l
+
δ
]
\left[l-\delta,l+\delta\right]
[l−δ,l+δ]中的点进行归并后,这些点在数组中关于y坐标已经有序,不妨设最终情况为
p
L
,
p
R
p_L,p_R
pL,pR两点分别位于点集
P
L
,
P
R
P_L,P_R
PL,PR,且该点对间距离小于
δ
\delta
δ,则
p
L
,
p
R
p_L,p_R
pL,pR两点一定位于下图中
2
δ
×
δ
2\delta\times\delta
2δ×δ的浅绿色矩形内。这是因为根据分析,当且仅当点对位于该浅绿色区域内时,两点间纵坐标之差小于
δ
\delta
δ,任何其他不在该范围内的点横坐标或纵坐标之差必定大于
δ
\delta
δ,距离必定大于
δ
\delta
δ,此时,该点对间距离一定不为最小,故无需进行比较。
此时,对于下图中矩形左半边的
δ
×
δ
\delta\times\delta
δ×δ正方形,因为
P
L
P_L
PL中所有点之间最小距离为
δ
\delta
δ,依鸽笼原理,该正方形内最多有4个点(如下图)。类似地,
P
R
P_R
PR中也最多有4个点可以位于该
δ
×
δ
\delta\times\delta
δ×δ正方形内。又因为两个正方形存在一条边完全重合,故有两个点为同一个点,因此在判断时只需判断每个点与其余6个基准点间距离关系即可。
(3)点集排序
对于算法中③合并子问题时,需要对点进行按照纵坐标的排序,此时不应直接在递归调用中使用排序算法进行排序。否则时间递归式将变为 T ( n ) = 2 × T ( n 2 ) + O ( n log n ) T\left(n\right)=2\times T\left(\frac{n}{2}\right)+O\left(n\log{n}\right) T(n)=2×T(2n)+O(nlogn),最终时间复杂度将变为 O ( n log 2 n ) O\left(n\log^2{n}\right) O(nlog2n)。因此需要使用类似归并排序的思路,即将已经排序的数组 Y l Y_l Yl和 Y r Y_r Yr进行合并形成有序数组。
3、时间复杂度分析
①使用了归并方法后对纵坐标的排序在递归过程中的每一层都为线性效率,在具体算法实现中对每次递归中按照纵坐标的排序与归并排序中合并操作十分类似。即按照纵坐标将有序数组
Y
L
,
Y
R
Y_L,Y_R
YL,YR进行合并从而形成有序数组Y。这是由于分治函数中当递归至仅有两个点时将直接合并并向上返回,因此,返回时待合并的两个数组
Y
L
,
Y
R
Y_L,Y_R
YL,YR是有小规模合并而来,因此
Y
L
,
Y
R
Y_L,Y_R
YL,YR一定是纵坐标有序的,继续合并后的数组也为纵坐标有序的数组,这样保证了每次操作一定范围内的点集时,点集内点都已经为纵坐标有序
通过分析算法,易得每次合并操作中比较的交换次数满足如下式子
1
2
(
r
i
g
h
t
−
l
e
f
t
)
≤
n
≤
r
i
g
h
t
−
l
e
f
t
\frac{1}{2}\left(right-left\right)\le n\le right-left
21(right−left)≤n≤right−left
对于每次规模为
n
=
r
i
g
h
t
−
l
e
f
t
n=right-left
n=right−left的合并操作,时间复杂度为线性的
O
(
n
)
O\left(n\right)
O(n)
②由①得,合并操作的时间复杂度为 O ( n ) O\left(n\right) O(n),对单个点的6个边界点的枚举操作时耗时为常数级,因此对单个点的枚举操作时间复杂度为 O ( 1 ) O\left(1\right) O(1)。则对n个点的枚举总时间复杂度为 O ( n ) O\left(n\right) O(n)
③不妨设程序消耗总时间为
T
(
n
)
T\left(n\right)
T(n),每层递归时间消耗为
t
(
n
)
t\left(n\right)
t(n)。由于在程序刚开始执行时需要对所有点进行按照横坐标的排序,需要消耗时间为
O
(
n
log
n
)
O\left(n\log{n}\right)
O(nlogn),则有:
T
(
n
)
=
t
(
n
)
+
O
(
n
log
n
)
T\left(n\right)=t\left(n\right)+O(n\log{n})
T(n)=t(n)+O(nlogn)
t
(
n
)
=
{
2
×
t
(
n
2
)
n>2
O
(
1
)
n <=2
t(n)= \begin{cases} 2\times t\left(\frac{n}{2}\right)& \text{n>2}\\ O\left(1\right)& \text{n <=2} \end{cases}
t(n)={2×t(2n)O(1)n>2n <=2
计算并化简得, T ( n ) ∝ n log n T\left(n\right)\propto n\log{n} T(n)∝nlogn即该算法的时间复杂度为 O ( n log n ) O(n\log{n}) O(nlogn)
4、编程实现
(1)距离计算函数
//定义计算距离函数
double dis(Point p1, Point p2)
{
return sqrt(pow((p1.x - p2.x), 2) + pow((p1.y - p2.y), 2));
}
(2)对点对进行以横坐标排序函数
bool cmpx(const Point &P1, const Point &P2)
{
return P1.x < P2.x;
}
(3)分治核心函数部分:
①当仅有一个点时,直接返回无穷:
//如果只有一个点则返回无穷
if (left == right)
{
return INF;
}
②当仅有两个点时,直接返回两点距离并对y进行排序
//如果有两个点则直接进行合并,并对y进行排序
if (left + 1 == right) {
if (point[left].y > point[right].y)
swap(point[left], point[right]);
return dis(point[left], point[right]);
}
③当点数大于二时,进行分治:
a.首先获得分治中点,并进行“分”的操作,并对点集进行以纵坐标的排序:
//如果点数大于2
int mid = (right + left) >> 1; //通过位操作快捷获得中点
int mid_x = point[mid].x;
min_dist = min(Divide(left, mid), Divide(mid + 1, right)); //缩小问题规模实现“分而治之”
Merge(left, mid, right); //以y进行排序
b.记录跨中线且距离分治中点水平距离小于当前最小值的异侧点
// 使用temp数组记录跨线异侧点,并进行6个边界点的判断
Point *temp = new Point[right - left + 1];
int i_size = 0;
for (int i = left; i <= right; i++) {
//如果点在异侧且在最小距离内则记录入temp数组
if (abs(point[i].x - mid_x) <= min_dist) {
temp[i_size++] = point[i];
}
}
c.对每个在temp数组中的点进行6个检测点的判断
for (int i = 0; i < i_size; i++) {
for (int j = i + 1; j < i_size && j < i + 6; j++) {
if ((temp[j].y - temp[i].y) > min_dist)
//如果两个点的纵坐标之差大于当前最小值,则一定不符合条件,直接进行下一组判断即可
break;
if (dis(temp[i], temp[j]) <= min_dist) {
//如果当前点距离小于等于当前最小值,则对最小距离进行更新
min_dist = dis(temp[i], temp[j]);
}
}
}
d.所有运算完成后,返回当前分治的最小距离即可
return min_dist;
(4)对点进行纵坐标排序:
首先完成对两侧点集的深复制,由于整个算法利用分治排序,可以借助分治排序的每次合并完成对点集的以纵坐标排序。由于排序前两侧点集已经有序,可以通过设置两侧点集的头指针对指针指向元素的值进行比较,并移动指针完成对所有点集的排序。当一侧点集排序完成后将另外一侧所有点直接顺序存入结果数组即可。此处编程实现过程中可以使用std::inplace_merge函数实现
void Merge(int left, int mid, int right)
{
//首先完成两侧点的深复制,
int n1 = mid - left + 1; //左侧点集大小
int n2 = right - mid; //右侧点集大小
Point *L = new Point[n1];
Point *R = new Point[n2];
//完成两侧点集的复制
for (int i = 0; i < n1; i++)
L[i] = point[left + i];
for (int i = 0; i < n2; i++)
R[i] = point[mid + 1 + i];
//由于整个算法利用分治法,可以利用类似归并排序完成点对的纵坐标排序
int i = 0, j = 0, k;
for (k = left; i < n1 && j < n2; k++)
{
if (L[i].y < R[j].y)
point[k] = L[i++];
else
point[k] = R[j++];
}
//如果归并时两侧数组存在剩余元素,则对剩余元素进行合并
while (i < n1)
point[k++] = L[i++];
while (j < n2)
point[k++] = R[j++];
}
(四)时间效率对比分析
正确性分析:
设计不同算法解决问题的基础是算法的正确性。通过随机数据生成了
10
5
{10}^5
105组大小为
10
4
{10}^4
104的测试数据分别使用暴力穷举法和分治法进行求解最短距离并比较。均证明暴力穷举法和分治法可以得到相同的正确答案。因此证明了算法的正确性。
时间效率分析:
在本次实验中,我利用随机数生成器,随机生成了10组数据并取平均值作为某给定数量级下的运行时间,对
10
1
{10}^1
101~
10
5
{10}^5
105数量级下的每组相同数据分别进行了暴力穷举与分治法两种算法的运行时间测试。分别整理做表总结展示如下:
1、暴力穷举法
数量级 | 平均时间 | 平均时间对数 | 理论时间 | 理论时间对数 |
---|---|---|---|---|
10 1 {10}^1 101 | 0.00025 | -3.60206 | 0.000278 | -3.55542 |
10 2 {10}^2 102 | 0.02805 | -1.55207 | 0.027834 | -1.55542 |
10 3 {10}^3 103 | 2.79846 | 0.446919 | 2.7834 | 0.444576 |
10 4 {10}^4 104 | 278.374 | 2.444629 | 278.374 | 2.444629 |
10 5 {10}^5 105 | 28042 | 4.447809 | 27837.4 | 4.444629 |
对于暴力穷举,由于算法未进行任何简化且算法执行次数与时间复杂度不随数据分布与数量级有任何变化,故整体拟合效果非常好,证明暴力法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)的结论正确。
2、分治法
数量级 | 平均时间 | 平均时间对数 | 理论时间 | 理论时间对数 |
---|---|---|---|---|
10 1 {10}^1 101 | 0.0043 | -2.36653 | 0.001078 | -2.967206 |
10 2 {10}^2 102 | 0.03371 | -1.47224 | 0.0215687 | -1.666176 |
10 3 {10}^3 103 | 0.38379 | -0.41590 | 0.3235305 | -0.490084 |
10 4 {10}^4 104 | 4.31374 | 0.63485 | 4.31374 | 0.634853 |
10 5 {10}^5 105 | 52.4283 | 1.71956 | 53.92175 | 1.731763 |
对于分治法,由于分治法需要开辟空间并实现递归操作,递归栈的迭代与开辟内存空间均需要消耗一部分时间。对于数量级较小的数据时,整个算法实际运行时间比较短,开辟内存空间与递归栈迭代消耗的时间的影响将被一定程度上放大。但当数量级较大时,这种影响被冲淡。因此会体现出当数量级较小时拟合效果较差,但当数量级较大时,拟合效果比较好。
3、两种算法分析对比
通过对算法时间的分析,可以看到,当对于小数量级的数据时(
1
0
2
10^2
102以下),暴力穷举法要优于分治法,但当数量级较大时,分治法明显优于暴力穷举法。这是由于分治法需要开辟内存空间并通过递归栈实现递归。因此将消耗更多时间,并对小数量级下的最终时间消耗产生较大影响。
因此对于“最近点对”问题最明智的方法是当数量级小于
10
2
{10}^2
102时选择暴力穷举法进行运算,当数量级大于
10
2
{10}^2
102时选择分治法进行运算。
四、实验结论或体会
1、设计不同算法解决问题的基础是算法的正确性。在测试不同算法的时候,一定要保证不同算法的正确性。在本实验中,可以通过暴力穷举来验证分治法答案的正确性。
2、对于一些数学类问题,都可以通过数学分析并借助分治法进行求解,从而缩短程序的运行时间。例如:逆序对问题。
3、算法的时间复杂度一般由若干次常数级时间组成,因此减少算法运行时间时也应降低算法的常数级时间。例如本次实验中最后再计算平方根,中间比较过程可以比较距离的平方值。
4、图形界面可视化可以更清晰的展示算法的实现过程,有助于提升对算法的理解。例如本实验内,我使用Python完成了图形的可视化界面。
5、对实验结论进行合理推广并举一反三,由二维的最近点问题推广至三维的最近点问题,并进行了算法验证。
五、思考
在实验的过程中,我发现了一下问题,思考查阅资料后提出相关解决办法如下:
1、算法优化
每次比较都是通过比较距离进行的。而获得距离的操作可以分为做差、平方、求和再开方四个步骤。不妨省去开方,直接比较距离的平方值,最后再开方。从而一定程度上节省常数运算的时间。
以暴力穷举法为例,我利用随机数生成器,随机生成了10组数据并取平均值作为某给定数量级下的运行时间,对
10
1
{10}^1
101~
10
5
{10}^5
105数量级下的每组相同数据分别进行了优化前与优化后的运行时间测试,并做表如下:
数量级 | 优化前 | 优化后 |
---|---|---|
10 1 {10}^1 101 | 0.00025 | 0.00022 |
10 2 {10}^2 102 | 0.02805 | 0.02496 |
10 3 {10}^3 103 | 2.79846 | 2.49062 |
10 4 {10}^4 104 | 278.374 | 247.752 |
10 5 {10}^5 105 | 28042 | 24957.38 |
可以看出,大约节省了10%左右的时间。算法成功被优化了
2、点按照纵坐标排序问题
在算法实现过程中,需要在分治时实现对点按照纵坐标的排序。对于每层排序,若使用快速排序等排序方法将造成时间浪费。若直接调用排序算法,则有
T
(
n
)
=
T
(
n
2
)
+
O
(
n
log
n
)
T\left(n\right)=T\left(\frac{n}{2}\right)+O(n\log{n})
T(n)=T(2n)+O(nlogn)
最后解得
T
(
n
)
=
O
(
n
log
2
n
)
T\left(n\right)=O(n\log^2{n})
T(n)=O(nlog2n),算法发生退化现象。不妨分析,在每层分治中,需要合并的两个子点集已经纵坐标有序,因此可以采用类似归并排序的方法实现每层排序。即设置两侧点集的头指针对指针指向元素的值进行比较,并移动指针完成对所有点集的排序。下移指针指向的元素较小的指针,直到遍历完整个点集。当一侧点集排序完成后将另外一侧所有点直接顺序存入结果数组即可。此时有
T
(
n
)
=
T
(
n
2
)
+
O
(
n
)
T\left(n\right)=T\left(\frac{n}{2}\right)+O(n)
T(n)=T(2n)+O(n)
解得最终时间复杂度为
O
(
n
log
n
)
O(n\log{n})
O(nlogn)节省了一定时间。
3、算法过程的可视化
为了对实验的点排序过程有更清晰深入的了解,我使用Python中的matplotlib函数库,实现了整个算法排序过程的可视化。清晰的展示了排序的具体过程。
4、分治法求最近点问题在三维下的推广
解决了分治法在二维下的最近点问题,尝试利用分治法解决三维下的最近点问题,发现有许多相似之处,进行编程实现并总结归纳。
(1)类比二维设计算法:
在解决三维的最近点问题时,不妨通过二维的最近点问题进行类比。
①使用分治法对空间进行划分:
类比二维的分治法,在三维坐标系V中,存在n个点依次记为
P
1
,
P
2
,
…
,
P
n
(
n
≥
3
)
P_1,P_2,\ldots,P_n(n\geq3)
P1,P2,…,Pn(n≥3)。为了将这
n
n
n个点尽可能均匀的划分到两个子空间
V
1
,
V
2
V_1{,V}_2
V1,V2中,可以构造一个垂直于
x
x
x轴的平面
x
(
P
)
=
k
x\left(P\right)=k
x(P)=k作为分割平面。其中
V
1
=
x
(
P
)
≤
k
x
[
P
1
∈
V
,
k
∈
N
]
和
V
2
=
x
(
P
)
>
k
x
[
P
1
∈
V
,
k
∈
N
]
V_1={x\left(P\right)\le kx[P_1\in V,k\in N]}和V_2={x\left(P\right)>kx[P_1\in V,k\in N]}
V1=x(P)≤kx[P1∈V,k∈N]和V2=x(P)>kx[P1∈V,k∈N]。通过递归算法,可以计算出
V
1
,
V
2
V_1{,V}_2
V1,V2中的最小点对距离
d
1
,
d
2
d_1{,d}_2
d1,d2,令
d
m
=
min
(
d
1
,
d
2
)
{d}_m=\min{(d_1{,d}_2)}
dm=min(d1,d2)
②分析跨界点:
第二步同样类比二维情况,求出
d
l
=
min
{
d
i
j
(
P
i
,
P
j
)
|
P
i
∈
V
1
,
P
j
∈
V
2
}
d_l=\min{\left\{d_{ij}\left(P_i,P_j\right)\middle| P_i\in V_1,P_j\in V_2\right\}}
dl=min{dij(Pi,Pj)∣Pi∈V1,Pj∈V2},即关于二分面的异侧点的最小距离。具体做法与二维分治类似:在第一次进行分割的原分割面
k
x
kx
kx的基础上做两个与之平行的切面
x
1
=
k
x
+
d
m
与
x
2
=
k
x
−
d
m
(
k
∈
N
)
x_1=kx+d_m与x_2=kx-d_m\left(k\in N\right)
x1=kx+dm与x2=kx−dm(k∈N),将属于两切面之间的点按照z轴坐标进行排序。故属于
x
1
≤
x
i
≤
k
x
x_1\le x_i\le kx
x1≤xi≤kx范围内的点
P
i
(
x
i
,
y
i
,
z
i
)
P_i\left(x_i,y_i,z_i\right)
Pi(xi,yi,zi)所能组成最短距离的另一个端点
P
j
(
x
j
,
y
j
,
z
j
)
P_j\left(x_j,y_j,z_j\right)
Pj(xj,yj,zj)一定存在以下关系:
k
x
≤
x
j
≤
x
2
kx\le x_j\le x_2
kx≤xj≤x2,同时易知,与
P
j
P_j
Pj组成最近点对的另一端点
P
i
P_i
Pi,一定在以
P
i
P_i
Pi为球心 ,以
d
m
d_m
dm为半径的半球面内,即有
{
(
x
i
−
x
j
)
2
+
(
y
i
−
y
j
)
2
+
(
z
i
−
z
j
)
2
≤
d
m
|
x
j
>
x
i
}
\left\{\sqrt{\left(x_i-x_j\right)^2+\left(y_i-y_j\right)^2+\left(z_i-z_j\right)^2}\le d_m\middle| x_j>x_i\right\}
{(xi−xj)2+(yi−yj)2+(zi−zj)2≤dm
xj>xi}。故只要找出上述两式的集合,即可得出需要进行判别的端点
P
j
P_j
Pj所属的范围。与二维平面上相似,由于
P
i
与
P
j
P_i与P_j
Pi与Pj中的点都具有稀疏性,因此,对于
P
i
P_i
Pi中的任意一点,
P
j
P_j
Pj中的点必定落在一个
d
m
∗
(
2
d
m
)
∗
(
2
d
m
)
d_m\ast\left(2d_m\right)\ast(2d_m)
dm∗(2dm)∗(2dm)的长方体中。即只需逐次比较
k
x
≤
x
j
≤
x
2
\ kx\le x_j\le x_2
kx≤xj≤x2空间与该外接长方体空间的交集空间内的所有点即可得到以
P
i
,
P
j
P_i,P_j
Pi,Pj为端点的最短距离。
③鸽笼法确定临界点
由于鸽笼原理,依据
P
i
P_i
Pi与
P
j
P_j
Pj中的点都具有稀疏性,在
d
m
∗
(
2
d
m
)
∗
(
2
d
m
)
d_m\ast\left(2d_m\right)\ast(2d_m)
dm∗(2dm)∗(2dm)的长方体中至多存在24个满足条件的临界点。因此只需对这
24
24
24个点进行距离检测即可,成功将每个点的距离比较时间复杂度从
O
(
n
)
O\left(n\right)
O(n)降至
O
(
1
)
O\left(1\right)
O(1)。
(2)算法设计
基于上面的分析,可以得到分治法求解三维最近点对问题的的伪代码如下:
设所有点的点集为
V
V
V,分治函数名为Merge(Point* V)
①当仅有一个点时直接返回INF,当仅有两个点时直接返回两个点的距离并进行归并。
②当有三个及以上点时,对
V
V
V中各点以
X
X
X轴坐标进行快速排序,令
k
k
k为
V
V
V中排序后的中间分治点,使得
V
1
=
{
x
(
P
i
)
≤
k
x
|
P
i
∈
V
,
k
∈
N
}
,
V
2
=
{
x
(
P
j
)
>
k
x
|
P
j
∈
V
,
k
∈
N
}
V_1=\left\{x\left(P_i\right)\le k x\middle| P_i\in V,k\in N\right\},V_2=\left\{x\left(P_j\right)>kx\middle| P_j\in V,k\in N\right\}
V1={x(Pi)≤kx∣Pi∈V,k∈N},V2={x(Pj)>kx∣Pj∈V,k∈N}
③令
d
1
=
M
e
r
g
e
(
V
1
)
,
d
2
=
M
e
r
g
e
(
V
2
)
d_1=Merge\left(V_1\right),d_2=Merge(V_2)
d1=Merge(V1),d2=Merge(V2)
④令
d
m
=
min
(
d
1
,
d
2
)
d_m=\min{\left(d_1,d_2\right)}
dm=min(d1,d2)
⑤构造两个空间
W
1
,
W
2
\ W_1,W_2
W1,W2,使得
W
1
=
k
x
−
d
m
≤
x
(
P
i
)
≤
k
x
∣
P
i
∈
V
,
k
∈
N
W_1={kx-d_m\le x\left(P_i\right)\le kx|P_i\in V,k\in N}
W1=kx−dm≤x(Pi)≤kx∣Pi∈V,k∈N
W
2
=
k
x
<
x
(
P
j
)
≤
k
x
+
d
m
∣
P
j
∈
V
,
k
∈
N
W_2={kx<x\left(P_j\right)\le kx+d_m|P_j\in V,k\in N}
W2=kx<x(Pj)≤kx+dm∣Pj∈V,k∈N
⑥对
W
1
,
W
2
W_1,W_2
W1,W2空间中的点分别以y轴坐标进行排序,得到点集
W
Y
1
,
W
Y
2
\ WY_1,WY_2
WY1,WY2,再分别以
z
\ z
z轴坐标进行排序,得到点集
W
Z
1
,
W
Z
2
\ WZ_1,WZ_2
WZ1,WZ2,此处需注意,仍然要采用类似归并排序的方法对点集进行排序,否则算法将退化
⑦对点集
W
Y
1
WY_1
WY1的对应点集
W
Y
2
WY_2
WY2,从小到大对其判别空间中的
24
24
24个临界点进行扫描,并返回最短距离
d
l
d_l
dl
⑧合并每层递归的结果,获得最终结果
d
m
i
n
d_{min}
dmin
(3)时间复杂度分析:
通过与二维下的分治法进行对比可知,三维与二维的查差别仅在于对
y
,
z
y,z
y,z都进行了排序,且由原来的
6
6
6个临界点变成了
24
24
24个临界点。故算法的时间复杂度仍然为
O
(
n
log
n
)
O(n\log{n})
O(nlogn)
附录
2024.04.27 更新了代码,解决了因为对y重新排序后跨界点中点下标失效问题。完整代码如下。
#include <algorithm>
#include <cmath>
#include <iostream>
#include <map>
using namespace std;
#define INF 0x3f3f3f3f
struct Point {
double x, y;
};
//定义计算距离函数
double dis(Point p1, Point p2) {
return sqrt(pow((p1.x - p2.x), 2) + pow((p1.y - p2.y), 2));
}
Point *point;
double min_dist;
bool cmpx(const Point &P1, const Point &P2) {
return P1.x < P2.x;
}
bool cmpy(const Point &P1, const Point &P2) {
return P1.y < P2.y;
}
double Divide(int left, int right) {
//如果只有一个点则返回无穷
if (left == right) {
return INF;
}
//如果有两个点则直接进行合并,并对y进行排序
if (left + 1 == right) {
if (point[left].y > point[right].y)
swap(point[left], point[right]);
return dis(point[left], point[right]);
}
//如果点数大于2
int mid = (right + left) >> 1;//通过位操作快捷获得中点
int mid_x = point[mid].x;
min_dist = min(Divide(left, mid), Divide(mid + 1, right));//缩小问题规模实现“分而治之”
inplace_merge(point + left, point + mid + 1, point + right + 1, cmpy);// 合并两个y有序的点数组
Point *temp = new Point[right - left + 1];// 使用temp数组记录跨线异侧点,并进行6个边界点的判断
int i_size = 0;
for (int i = left; i <= right; i++) {
//如果点在异侧且在最小距离内则记录入temp数组
if (abs(point[i].x - mid_x) <= min_dist) {
temp[i_size++] = point[i];
}
}
for (int i = 0; i < i_size; i++) {
for (int j = i + 1; j < i_size && j < i + 6; j++) {
if ((temp[j].y - temp[i].y) > min_dist)
//如果两个点的纵坐标之差大于当前最小值,则一定不符合条件,直接进行下一组判断即可
break;
if (dis(temp[i], temp[j]) <= min_dist) {
//如果当前点距离小于等于当前最小值,则对最小距离进行更新
min_dist = dis(temp[i], temp[j]);
}
}
}
return min_dist;
}
int main() {
int length;
cin >> length;
point = new Point[length];
for (int i = 0; i < length; i++) {
cin >> point[i].x >> point[i].y;
}
double ans = INF; //设定初始结果为无穷大
for (int i = 0; i <= length - 1; i++) {
for (int j = i + 1; j <= length - 1; j++) {
ans = min(ans, dis(point[i], point[j]));
}
}
cout << "force ans:" << ans << endl;
sort(point, point + length, cmpx); // 先对x排序
Divide(0, length);
cout << "Divide ans:" << min_dist << endl;
return 0;
}