分治法解最近对问题
题目描述
Description
最近对问题:使用分治算法解决最近对问题。
Input
第一行为测试用例个数。后面每一行表示一个用例,一个用例为一些平面上点的集合,点与点之间用逗号隔开,一个点的两个坐标用空格隔开。坐标值都是正数。
Output
对每一个用例输出两个距离最近的点(坐标使用空格隔开),用逗号隔开,先按照第一个坐标大小排列,再按照第二个坐标大小排列。如果有多个解,则按照每个解的第一个点的坐标排序,连续输出多个解,用逗号隔开。
Sample Input 1
1
1 1,2 2,3 3,4 4,5 5,1.5 1.5
Sample Output 1
1 1,1.5 1.5,1.5 1.5,2 2
题目解析
这道题说实话挺麻烦的.
就是找到距离最近的点对集合并输出
思路解析
思路1:暴力搜索,复杂度O(N2)
两两个点遍历,遍历中更新最小距离与点集,
思路2:分治,复杂度O(NlogN2)
关于分治的一些基本思路可以看博客,该博客可以很好的提供入门思想,但该博客存在着一些问题
之所以是O(NlogN2)是因为在并归中又使用了快速排序
该算法在求出最近点对时速度很慢,也没有进行优化,也没有解释为什么要对y进行排序,其有问题的代码部分如下所示:
for (i = 0; i < k; i++)
for (j = i + 1; j < k && S[temp[j]].y - S[temp[i]].y < d; j++)
{
double d3 = dist(temp[i], temp[j]);
if (d > d3)
d = d3;
}
将其优化后可以将该部分代码的复杂度变为线性,优化后是O(NlogN2),如何优化见下个思路
简而言之,该部分思路如下:
-
将原始数据点集按照x坐标进行排序,好根据x进行点集划分
-
划分,也就是并归过程
(1)求中位线,将点集以中位线为界,分为左右两部分点集。
(2)继续找左右两部分的中位线,继续划分。
(3)直到中位线左右两部分的点的个数小于或等于二为止 -
求点集中最小距离
(1)求中位线左边部分点集中最小距离d1,求中位线右边部分点集中最小距离d2;
(2)dmin =min(d1,d2);
(3)选出到中位线的距离均小于min的点,计算该部分点集中最小距离d3,dmin=min(dmin ,d3)。
思路3:分治,复杂度O(NlogN),是对思路2的优化
再理解了之前那一篇博客后,可以看这篇博客,主要理解如何使用并归特性代替快速排序以及如何加快求出最近点对,但该博客说的不是很清晰,这里总结一下
-
为什么要对y进行排序?
对y排序可以快速求出最近点对,至于为什么见3
-
如何使用并归特性代替快速排序?
即在原有的并归代码(merge)上,增加排序(sort_y)的代码,如果只看排序部分代码的话其实就是一个并归排序,
-
如何加快求出最近点对
我们只需要搜索离当前点纵坐标距离最近的6个点即可,而无需遍历该点集中全部的点.因此要先排序选出最近的6个点,那为什么只搜索6个点就可以了呢?总结作者的话如下
- 左点集中最近点对距离为d1,右点集中最近点对距离为d2,因此dmin =min(d1,d2);
- 选出到中位线的距离均小于min的点,称该部分为中间点集
- 若中间点集中存在点对距离d3,并且满足d3<dmin(这个条件就是我们要寻找的) ,那么这两点的y值之差必须小于dmin (若两点之间距离小于a,两点的纵坐标差一定也小于a)
- 因此令大矩形长为2*mindis,宽为mindis,我们只需要在这个范围内寻找小于dmin的d3
- 因此令点A位于矩形上边界,那么所有与A有可能构成最近点对的候选点应位于矩形候选区域中,我们现在就对这个矩形区域做分析
- 大矩形长为2 * mindis,宽为mindis,可以分成两个长为mindis,宽为mindis的小矩形,并且这两个矩形交集部分(中间那条线的点)归为左半边的矩形(这里一定要注意,重合的点归为左半边)
- 现在我们要往矩形中,尽可能的多装点,以发现这个矩形所容纳的点是有限的
- 每个小矩形中的点一定是满足相互之间的距离dx>=dmin的,不然在第一点中,我们的dmin就会更新为dx(这点很重要!!)
- 要想往矩形内部多装点,且满足上述条件,那么第一个小矩形可以放4个顶点,第二个小矩形可以放在2个顶点和重合的边上,见图一中7个黄色点(如果黄色点偏左,证明在左边的小矩形内,右边的矩形只有三个点,且只要第三点在重合线上,都满足条件,为什么不在顶点上是因为会和左边的小矩形重复)
- 若将A点(黑色点)放入,并且满足上述条件,那么可放的点最多的情况A点位置见图2所示,此时只有6个点
- 因此得出结论,将点按照y排序后,只需要看距离最近的6个点即可,
如果还是不理解,参考最多6个点,反证法
(图片来自引用博客)
代码实现
暴力搜索1
def add_element(min_list, p1, p2):
min_list.append(p1[0] + " " + p1[1] + ",")
min_list.append(p2[0] + " " + p2[1] + ",")
def cal_dist(p1, p2):
return (float(p1[0]) - float(p2[0])) ** 2 + (float(p1[1]) - float(p2[1])) ** 2
if __name__ == '__main__':
# s = "1 1,2 2,3 3,4 4,5 5,1.5 1.5"
l = []
for _ in range(int(input())):
# l.append(input().strip().split(" "))
s = input()
l = list(map(lambda x: str(x).split(" "), s.split(",")))
l = sorted(l, key=lambda x: (float(x[0]), float(x[1]))) # 先排序
n = len(l)
min_num = -1
min_list = []
for i in range(n):
for j in range(i + 1, n):
p1, p2 = l[i], l[j]
dis = cal_dist(p1, p2)
if i == 0 and j == 1:
min_num = dis
add_element(min_list, p1, p2)
break
if dis < min_num:
min_num = dis
min_list.clear()
add_element(min_list, p1, p2)
elif dis == min_num:
add_element(min_list, p1, p2)
else:
continue
print("".join(min_list)[:-1])
暴力搜索2(自欺欺人分治)
def add_element(min_list, p1, p2):
if p1[0] > p2[0]:
p2, p1 = p1, p2
if p1[0] == p2[0] and p1[1] > p2[1]:
p2, p1 = p1, p2
min_list.append(p1[0] + " " + p1[1] + ",")
min_list.append(p2[0] + " " + p2[1] + ",")
def fun(arr1, arr2, min_num, min_list):
# print(arr1, arr2)
for p1 in arr1:
for p2 in arr2:
dis = cal_close(p1, p2)
if dis < min_num[0]:
min_num[0] = dis
min_list.clear()
add_element(min_list, p1, p2)
elif dis == min_num[0]:
add_element(min_list, p1, p2)
else:
continue
return arr1 + arr2
def cal_close(p1, p2):
return (float(p1[0]) - float(p2[0])) ** 2 + (float(p1[1]) - float(p2[1])) ** 2
def merge_sort(i, j, arr, min_num, min_list):
mid = (i + j) >> 1
if i == j:
return [arr[i]]
arr1 = merge_sort(i, mid, arr, min_num, min_list)
arr2 = merge_sort(mid + 1, j, arr, min_num, min_list)
return fun(arr1, arr2, min_num, min_list)
if __name__ == '__main__':
# s = "1 1,2 2,3 3,4 4,5 5,1.5 1.5"
for _ in range(int(input())):
s = input()
l = list(map(lambda x: str(x).split(" "), s.split(",")))
num = [cal_close(l[0], l[1])]
lis = []
lis.append(l[0][0] + " " + l[0][1] + ",")
lis.append(l[1][0] + " " + l[1][1] + ",")
merge_sort(0, len(l) - 1, l, num, lis)
print("".join(lis)[:-1])
分治(只计算了距离,没有收集点对),如果在此基础上添加收集点对的话,要增加不少代码…
import sys
# 计算距离
def cal_dist(p1, p2):
return (float(p1[0]) - float(p2[0])) ** 2 + (float(p1[1]) - float(p2[1])) ** 2
# 这里就是并归排序,只是排序的依据是点y值得大小
# 这里排序要改变原始数组arr的值,后续代码中要用到arr
def sort_y(l, r, arr):
res = []
mid = (l + r) >> 1
i, j = l, mid + 1
while i <= mid and j <= r:
if arr[i][1] < arr[j][1]: # 比较y值大小
res.append(arr[i])
i += 1
else:
res.append(arr[j])
j += 1
while i <= mid:
res.append(arr[i])
i += 1
while j <= r:
res.append(arr[j])
j += 1
for i in range(0, r + 1 - l):
arr[l + i] = res[i]
#
def merge(l, r, arr):
if l == r:
return sys.maxsize # 如果是一个点则返回最大值
if l + 1 == r:
sort_y(l, r, arr) # 两个点这里要并归一次
return cal_dist(arr[l], arr[r]) # 返回距离
mid = (l + r) >> 1
d1 = merge(l, mid, arr)
d2 = merge(mid + 1, r, arr)
d = min(d1, d2) # 更新左右距离中较小的
sort_y(l, r, arr) # 这里之后的arr[l,r]是按照y排序的了
temp = []
for i in range(l, r + 1):
if abs(arr[i][0] - arr[mid][0]) <= d:
temp.append(i) # 寻找距离小于d的点
#
# temp.sort(key=lambda x: arr[x][1]) # 按照纵坐标排序 TODO:
# 求出temp中最近的一对点
for i in range(len(temp)): # 所以这里的temp是已经按照y排过序的了
for j in range(i + 1, len(temp)):
if arr[temp[i]][1] - arr[temp[j]][1] > d: # 如果纵坐标之差大于了目标距离,那就不用算了
continue
if j > i + 1 + 6: # 如果大于6个点就停止
break
d3 = cal_dist(arr[temp[i]], arr[temp[j]]) # 计算距离
d = min(d, d3) # 更新较小值
return d
if __name__ == '__main__':
# s = "1 1,2 2,3 3,4 4,5 5,1.5 1.5"
l = []
for _ in range(int(input())):
s = input().strip().split(" ")
l.append([float(s[0]), float(s[1])])
# l = list(map(lambda x: [float(x.split(" ")[0]), float(x.split(" ")[1])], s.split(",")))
# l = sorted(l, key=lambda x: (x[0], x[1])) # 先排序
l = sorted(l, key=lambda x: (x[0])) # 先排序
ans = merge(0, len(l) - 1, l)
print(round(ans ** 0.5, 4))