实验内容:
- 对于平面上给定的N个点,给出所有点对的最短距离,即,输入是平面上的N个点,输出是N点中具有最短距离的两点。
- 要求随机生成N个点的平面坐标,应用蛮力法编程计算出所有点对的最短距离。
- 要求随机生成N个点的平面坐标,应用分治法编程计算出所有点对的最短距离。
- 分别对N=100000—1000000,统计算法运行时间,比较理论效率与实测效率的差异,同时对蛮力法和分治法的算法效率进行分析和比较。
1.暴力枚举算法
(1)主要思路:枚举点集中所有可能形成的点对,且设置全局变量最小距离mindist,然后计算每个点对之间的距离并与mindist进行比较,如果比mindist还小,就对mindist进行更新,同时对最近点对的两个点也进行更新。
(2)时间复杂度:点集中有n个点,可能形成的点对有O(n^2)的规模,即需要两层for循环,需要一一进行枚举,因此时间复杂度为O(n^2)。
(3)伪代码:
暴力枚举算法O(n^2) |
Function Brute_n2(left,right) For i=left to right For j=i+1 to right If dist(a[i],a[j]) < mindist mindist=dist(a[i],a[j]) ansX1=a[i].id ansX2=a[j].id ansP[0]=a[i] ansP[1]=a[j] |
(4)算法过程:
方格内的数字表示两点之间的距离,红色表示目前更新到的最短距离,蓝色表示最短距离的两个点。
随机生成10个点:points = [(943, 5798), (9838, 8020), (5975, 5678), (5814, 8662),
(2067, 7698),(5009, 2238), (4297, 5729), (8897, 378), (2003, 1453), (2317, 194)]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 0 | 9168.33 | 5033.43 | 5650.59 | 2207.57 | 5404.25 | 3354.71 | 9625.1 | 4472.43 | 5769.98 |
1 | 0 | 4517.49 | 4074.89 | 7777.67 | 7533.31 | 5995.95 | 7699.72 | 10233.1 | 10854.1 | |
2 | 0 | 2988.34 | 4399.19 | 3573.06 | 1678.77 | 6052.11 | 5798.91 | 6592.06 | ||
3 | 0 | 3869.02 | 6474.24 | 3302.09 | 8839.09 | 8154.35 | 9161.66 | |||
4 | 0 | 6202.17 | 2974.87 | 10011.6 | 6245.33 | 7508.16 | ||||
5 | 0 | 3562.87 | 4310.01 | 3106.81 | 3380.06 | |||||
6 | 0 | 7056.43 | 4852.49 | 5878.49 | ||||||
7 | 0 | 6977.31 | 6582.57 | |||||||
8 | 0 | 1297.57 | ||||||||
9 | 0 |
如上表所示,mindist =1297.57,ansX1=8,ansX2=9,即第8个点和第9个点为最近点对,最短距离为1297.57。
2.分治算法
(1)主要思路:
用分治算法求解最近点对的问题分为三个阶段:排序、递归、合并,其中核心是我们的递归函数。以下是它们的主要思路:
- 合并(Merge) 函数:用于合并两个已经排序好的子数组。在这里,按照点的纵坐标进行归并排序,合并得到排好序的数组。至于为什么选择归并排序而不是快速排序(实测数据证明下文有),主要是因为归并排序是一种稳定的排序算法,它能够保持相同元素的相对顺序在排序前后不变,快速排序虽然在平均情况下具有较好的性能,但它是一种不稳定的排序算法,元素的相对顺序可能会被改变,这在合并阶段可能会导致不正确的结果。
- 入口(Divided_nlogn) 函数:用于初始化并调用递归函数。首先对整个点集按照横坐标进行排序,然后调用递归函数进行分治求解。
- 递归(Recur) 函数:用于分治地处理问题。首先判断当前区间是否只有一个点,如果是则返回。然后,找到当前区间的中间点,将问题分成两个部分,第一部分是求出子问题即左半部分left点集的最近点对和右半部分right点集的最近点对;第二部分是求出横跨左右两个点集的点形成的最近点对。这样就实现了找到点集left-right所有可能形成的最近点对。
(2)时间复杂度:
总时间复杂度为排序阶段的时间复杂度加上递归合并阶段的时间复杂度,即O(nlogn)+O(nlogn),最后结果为O(nlogn)。
(3)伪代码:
分治算法O(nlogn) |
|
(4)算法过程:
最近点对的线段用红色实线绘制,中间线用蓝色实线绘制,当前处理区间边界线Left和Right用两条红色虚线表示,midx - mindist 和 midx + mindist用两条蓝色虚线表示。用黄色线段表示当前处理的点对。
随机生成10个点:points = [(943, 5798), (9838, 8020), (5975, 5678), (5814, 8662),
(2067, 7698),(5009, 2238), (4297, 5729), (8897, 378), (2003, 1453), (2317, 194)]
分治算法过程(以更新最近点对的图为例)
![]() | ![]() | ![]() | ![]() |
Left=943 Mid=943 Right=2003 Dist=4472.429 最近点对信息: P1:(943,5798) P2:(2003,1453) Mindist=4472.43 | Left=943 Mid=2003 Right=2067 Dist=2207.572 最近点对信息: P1:(2067,7698) P2:(943,5798) Mindist=2207.57 | Left=943 Mid=2067 Right=4297 Dist=1297.566 最近点对信息: P1:(2003,1453) P2:(2317,194) Mindist=1297.57 | Left=943 Mid=4297 Right=9838 Dist=0.000 最近点对信息: P1:(2003,1453) P2:(2317,194) Mindist=1297.57 |
需要注意的是,当没有找到最近点对的时候p1-p2=--,且p1和p2的距离dist=0.000。上述只以更新最近点对时的图为例,完整过程在动图展示中。最终得到的最近点对为P1(2003,1453)和P2(2317,194),最近点对距离Mindist=1297.57。
3.分治算法优化
(1)底层处理:
- 考虑“底层处理”,在算法递归到底层时,当集合中的点数量减少到一个较小的阈值(三个点)时,不再进行继续的递归分割,而是直接使用暴力方法O(n^2)求解最近点对。
- 具体而言,当分治算法递归到底层时,可能会剩下很少的点。在这种情况下,继续递归下去可能会增加额外的开销,因为递归的开销在剩下的点很少时可能会占据相当大的比例。在点数量较少时,暴力方法的计算开销相对较小,而不会明显影响算法的性能。这种优化策略可以减少递归调用的次数,降低算法的时间复杂度,提高算法的执行效率。
- 伪代码如下:
分治算法-底层优化处理 |
|
(2)限界优化:
- 考虑在跨点集处理中的第三步对S(pi)进行限界优化处理。
- 在处理点集S(pi)时,通过确定中值线midx,将点集分为左右两部分S1(pi)和S2(pi),然后在处理S2(pi)时,只需要考虑纵坐标最大的两个点,而不需要考虑全部点,从而限制了搜索空间,提高了算法的效率。这种处理方式被称为限界优化,或者说边界优化的一种形式。
- 处理思路:以midx直线为中值,将点集S(pi)分成左右两个部分S1(pi)和S2(pi),现假设已有pi在左半部分点集S1(pi)中,猜想如果点集S2(pi)存在点pj,使得pi和pj的距离可以更新答案。那么pj一定是点集S2(pi)里纵坐标最大的点或纵坐标次大的点。样本要逐个遍历点集S2(pi)中的点(最多四个点),就可以变成只需要遍历S2(pi)中的前两个点(纵坐标最大的两个点)。
- 伪代码如下:
分治算法-限界优化处理 |
|
实验结果及分析:
1.暴力枚举算法O(n^2):
可以看出,暴力枚举算法O(n^2)的实际运行时间曲线略低于预测运行时间曲线,但总体来说两条曲线是比较贴合的,即实际性能与预测性能相差不大。
2.分治算法O(nlogn):
可以看出,分治算法O(nlogn)的实际运行时间曲线和预测运行时间曲线几乎是完全贴合,即预测效果是不错的,预测性能与实际性能几乎一致。
3.O(n^2)与O(nlogn)对比:
根据上面暴力法和分治法的效率分析,我们易知暴力法和分治法的时间效率相差很大,而且暴力法在数据规模很大时,运行时间非常慢,因此为了方便比较,以二者的预测运行时间来进行对比,同时把时间取对数。下图数据和图表的时间表示均已取过对数。
得到两种算法的差距非常明显,分治法的运行效率明显比暴力法的运行效率优越。
4.分治算法合并阶段使用快速排序和归并排序性能对比:
一开始当n的规模较小时,二者的差距不算大,但是随着n的规模增大,使用归并排序的算法性能明显优于使用快速排序的算法性能。当n=1000000时,使用快速排序所需要的运行时间已经是使用归并排序的运行时间的2倍了。我们知道归并排序的时间复杂度稳定在 O(nlogn) 级别,而快速排序在最坏情况下可能会达到 O(n^2),所以这使得归并排序在处理大规模数据时更具优势。
5.分治算法优化性能对比:
由图可知,普通的分治算法最慢,只有限界优化后的分治算法略快一点,只有底层处理后的分治算法快得多一些,底层处理加上限界优化后的分治算法最快。
- 普通的分治算法最慢,因为它在任何情况下都会继续递归地划分问题,导致递归调用过多,增加时间复杂度。
- 限界优化后的分治算法略快一点,但不是很多,因为优化点集时,点集本来最多也才4个,优化后最多2个,数量差距不是很大,但由于减少遍历次数,也一定程度上优化了算法效率。
- 底层处理后的分治算法略快一些,因为当问题规模较小时,它不再进行进一步的递归划分,而是直接使用暴力方法解决,减少了递归调用次数。
- 底层处理加上限界优化后的分治算法最快,因为它综合了底层处理和限界优化,避免了不必要的递归调用,并通过优化点集的处理方式进一步降低了时间复杂度,从而提高了算法的效率。