大家好,我们今天来看一道非常非常经典的算法题——最近点对问题

这个问题经常在各种面试当中出现,难度不低,很少有人能答上来。说实话,我也被问过,因为毫无准备,所以也没有答上来。是的,这道题有点神奇,没有准备的人往往答不上来。

题意

我们先来看下题意吧,题意很简单,在一个平面当中分布着n个点。现在我们知道这n个点的坐标,要求找出这n个点当中距离最近的两个点的间距。

我不确定这个问题是否出自于天文学,但是把它放到天文的背景当中非常合适。想象一下在浩瀚的宇宙当中,存在着无数的星辰,我们想要找到其中距离最近的两颗天体。它们有可能是双子星,也有可能是伴星系……这么想想,有没有觉得很浪漫?

我们来分析一下问题,会发现一个矛盾之处。矛盾的地方在于如果我们要求出每两个点之间的距离,那么复杂度一定是,因为n个点取两个点一个有种可能。如果存在更快的算法,那么势必我们不能求出所有点对之间的距离,但如果我们连所有的距离都没有枚举过,如何可以判断我们找到的一定是对的呢?

我当时在面试的时候就是这么回答的,虽然我们现在知道这个说法是错的,但是如果没有这一层信息,你还能判断吗?

分治法

如果我们仔细思考一下,会发现这个问题和排序其实非常类似。因为我们在排序的时候,表面上来看每两个点之间都存在大小关系,我们要排序似乎也要获得这些关系。但实际上,我们都知道,无论是快速排序还是归并排序都可以做到的时间内完成排序。

无论是快速排序还是归并排序,本质上都是利用的分治法。那么这道题是否也可以使用分治法求解呢?

答案当然是可以的,既然是使用分治法,那么我们首先要做的就是拆分,将整个的数据拆成两个部分,使用递归分别完成两个部分,然后再合并得到完整的结果。在这个问题当中,我们要拆分数据非常简单,只需要按照x轴坐标对所有点进行排序,然后选择中点进行分割即可,分割之后我们得到的结果如下:

拆分结束之后,我们只需要分别统计左边部分的最近点对、右边部分的最近点对,以及一个点在左边一个点在右边的最近点对即可。对于前面两种情况都很好解决,我们只需要递归就可以搞定了,但对于第三种情况应该怎么办?这也是本题的难点所在。

要分析清楚这个问题不是非常容易,需要深入的思考,首先我们通过递归调用可以获得左边部分SL的最短距离D1以及右边部分SR的最短距离D2。我们取,也就是左右两边最短距离的最小值,这个应该很好理解。

求出了D之后,我们就可以用它来限定一个点在SL一个点在SR这种情况的点对的范围了,不然的话我们要比较两边各有n/2个点的情况,依然计算复杂度很大。

我们来分析一下问题,我们在左侧随便选择一个点p,我们来想一个问题,对于点p而言,SR一侧所有的点都有可能与它构成最近点对吗?当然不是,有一些离得远的是明显不可能的,对于这些点我们没有必要一一遍历,直接都可以批量忽略。要想和p点构成最近点对,必须在下图这个虚线框起来的范围内

这个虚线构成的框是一个长方形,它的宽是D,长是2D。这是怎么来的呢?其实很简单,对于p点来说,要想和他构成全局的最近点对,那么距离它的距离一定要小于目前的最优解D。既然距离要小于D,那么意味着它们的横纵坐标之差的绝对值必须也要小于D。

当然这个框只是我们直观看到的,在实际算法运行的时候是没有这个框的,我们只能根据坐标轴自己去进行判断某个点在不在框里。

有了这个框之后,我们产生了另外一个问题,那就是这个框到底可以起到多大的作用呢?有了这个框就可以降低算法复杂度了吗?会不会出现右侧所有点都在框里的极端情况呢?

其实我们只需要简单分析一下就会发现这是不可能的,不仅可以判断出这是不可能的,而且我们还可以得出另外一个非常非常惊人的结论。

首先,我们来论证一下为什么右侧所有的点都落在这个虚线框里是不可能的。我们先来看最极端的情况,最极端的情况就是我们选中的p点就在分割线上。那么以它画出来的框应该全部都落在SR区域,画成图大概是这样的:

但是我们简单想一下会发现一个问题,就是这个虚线框里的点的数量不可能是无限的。因为对于框里的点我们有一个基本的要求,就是在这个框里并且在SR区域内的点,两两之间的距离不得小于D。如果小于D了就和我们刚才得到D是左右两侧最小距离的结论矛盾了。那么上面图中的情况其实是不可能的,因为这么多点聚集在一起明显存在两个点的距离小于D了,这就矛盾了。

也就是说由于存在这个距离的限制,能够落在这个虚线框里的点的数量是有限的,而且这个数量比大家想的也许要小得多,有多小呢?小到最多只有6个,也就是下面这种情况:

在上图当中,一共有6个点,这6个点两两之间的最短距离是D,这是最极端的情况。无论我们如何往其中加入点,都一定会产生两个点之间的距离小于D。这是我们很直观的感受,有没有办法证明呢?其实也是有的,我们可以把这个矩形进行六等分变成下图这样:

我们来分析一下,上图的每一个小矩形的长是,宽是,它的对角线长度是。那么根据鸽笼原理,如果我们放入超过6个点,必然会存在一个小矩形内存在两个点。而小矩形内最大的距离小于D,也就是说这两个点的距离必然也小于D,这就和我们之前的假设矛盾了,所以可以得出超过7个点的情况是不存在的。

也就是说对于SL侧的点p,我们在SR侧最多只能找出6个点来可能构成最短点对,这样我们需要筛查的点对数量就大大减小。并且对于SL侧的点来说,并不是所有的点都需要考虑的,只有和中点O横坐标差值小于D的点才需要考虑

表面上看起来我们所有的分析都结束了,但实际上还有一个问题没有解决。就是我们怎么样找到这6个点呢?显然只根据横坐标是不行的,这个时候就需要考虑纵坐标了。我们将点集分成左右两个部分之后,对右侧部分按照纵坐标进行排序,对于左侧的点(x, y)而言,我们只需要筛选出右侧满足纵坐标范围在(y - d, y + d)范围内的点,这样的点最多只有6个。我们可以利用二分法找到纵坐标大于 y - d的最小的点,然后依次枚举之后的6个点即可。

代码实现

在我们实现算法之前,我们需要先生成测试数据,否则如何验证我们的算法是否有问题呢?而且这个算法也是我从头开发的,对于debug也有帮助。

在这道题当中,测试数据还是比较简单的,只需要生成两个随机数作为坐标即可。我们调用这个方法先生成200个点作为测试。

import random

def random_point():
    x, y = random.uniform(0, 1000), random.uniform(0, 1000)
    return (x, y)

points = [random_point() for _ in range(200)]

接着我们再实现暴力解法,用来检测我们的算法的正确性,这一段我想应该不用我多说,大家都能理解。

def distance(x, y):
    return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1])** 2)

def brute_force(points):
    ret = sys.maxsize
    a, b = None, None
    n = len(points)
    for i in range(n):
        for j in range(i+1, n):
            dis = distance(points[i], points[j])
            if dis < ret:
                ret = dis
                a, b = i, j
    return ret, points[a], points[b]

最后是重头戏了,其实算法本身的代码量并不大,但是其中的细节不少,稍有不慎就可能翻车。

def divide_algorithm(points):
    n = len(points)
    # 特判只有一个点或者是两个点的情况
    if n < 2:
        return sys.maxsize, None, None
    elif n == 2:
        return distance(points[0], points[1]), points[0], points[1]

    # 对所有点按照横坐标进行排序
    points = sorted(points)
    half = (n - 1) // 2
    # 递归,这里有一个问题,为什么要先排序再递归?
    d1, a1, b1 = divide_algorithm(points[:half])
    d2, a2, b2 = divide_algorithm(points[half:])
    d, a, b = (d1, a1, b1) if d1 < d2 else (d2, a2, b2)

    calibration = points[half][0]
 
    # 根据中间的位置将点集分成两个部分
    left, right = [], []
    for u in points:
        if calibration - d < u[0] < calibration:
            left.append(u)
        elif calibration <= u[0] < calibration + d:
            right.append(u)

    # 右侧点集按照纵坐标排序
    right = sorted(right, key=lambda x: (x[1], x[0]))

    res = d

    for u in left:
        # 左开右闭的二分
        l, r = -1, len(right)-1
        while r - l > 1:
            m = (l + r) >> 1
            if right[m][1] <= u[1] - d:
                l = m
            else:
                r = m

        idx = r
  
        # 在范围内最多只有6个点能够构成最近点对
        for j in range(7):
            if j + idx >= len(right):
                break
            if distance(u, right[idx + j]) < res:
                res = distance(u, right[idx + j])
                a, b = u, right[idx + j]

    return res, a, b

算法是实现完了,但是仍然有一些细节,比如说为什么我们在分治的时候需要先排序再递归呢?直接分成两个部分递归行不行?为什么不行?比如我们二分的时候使用的是左闭右开的区间二分?

这两个问题我先不给出答案,希望大家能够自己尝试着思考一下。如果有想法的话,欢迎在下方给我留言讨论。

今天的文章就到这里,衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发

原文链接,求个关注

经典分治问题,平面N个点求最近点对的更多相关文章

  1. hdu 1007 Quoit Design 分治求最近点对

    Quoit Design Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Tot ...

  2. hdu 1007 Quoit Design(分治法求最近点对)

    大致题意:给N个点,求最近点对的距离 d :输出:r = d/2. // Time 2093 ms; Memory 1812 K #include<iostream> #include&l ...

  3. hdu 1007 Quoit Design (经典分治 求最近点对)

    Problem Description Have you ever played quoit in a playground? Quoit is a game in which flat rings ...

  4. ZOJ 2017 Quoit Design 经典分治!!! 最近点对问题

    Quoit Design Time Limit: 5 Seconds      Memory Limit: 32768 KB Have you ever played quoit in a playg ...

  5. HDU 1007:Quoit Design(分治求最近点对)

    http://acm.hdu.edu.cn/showproblem.php?pid=1007 题意:平面上有n个点,问最近的两个点之间的距离的一半是多少. 思路:用分治做.把整体分为左右两个部分,那么 ...

  6. C++入门经典-例4.4-循环嵌套之求n的阶乘

    1:代码如下: // 4.4.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> using ...

  7. Java经典案例之用三种方法求1~100以内素数之和

    素数,不能被除了1和本身以外整除的数被称为素数.接下来我用三种方式求得1~100以内素数. 方式一 外层每循环一次,内层就计算出这个数有几个因子,我们都知道素数的因子只有两个,所以如果个数为2就加进总 ...

  8. (hdu1007)Quoit Design,求最近点对

    Problem Description Have you ever played quoit in a playground? Quoit is a game in which flat rings ...

  9. 求最近点对算法分析 closest pair algorithm

    这个帖子讲得非常详细严谨,转一波. http://blog.csdn.net/lishuhuakai/article/details/9133961

随机推荐

  1. HTTPS证书知识扫盲

    1. 前言 现在搞网站域名不加个HTTPS就显得不专业,特别在使用JWT进行认证的接口一定要加HTTPS为你的接口增加一层安全屏障.今天就来聊聊配置HTTPS的关键SSL证书,也被称为CA证书. 2. ...

  2. Flask实现websocket

    from flask import Flask,request user_socket_dict = {} app = Flask(__name__) @app.route("/conn_w ...

  3. LeCun自曝使用C语言23年之久,2年前才上手Python,还曾短暂尝试Lua!

    程序员圈子的流行风潮,过几年就怀旧风走一波. 这不,最近Twitter上刮起了一阵编程语言使用历史的风潮. 连图灵奖得主.CNN之父-- Yann LeCun 也参与进来了. 他自曝使用C语言时间最长 ...

  4. 【C语言C++编程学习笔记】基础语法,第一个简单的实例编程入门教程!

    C语言/C++编程学习:一个简单的实例 让我们来看一个简单的C语言程序.从下面的程序可以看出编写C语言程序的一些基本特征.   如果你能知道该程序将会在显示器上显示一些内容,那说明你还是知道一些的! ...

  5. 【博弈论】CF 1215D Ticket Game

    题目大意 洛谷链接 给出一个长度为\(n\)的由数字组成的字符串(\(n\)是偶数).但可能有偶数个位上的数字为?. 现在有两个人\(A\)和\(B\),在?的位置上填\(0\)~\(9\)的数,一直 ...

  6. linux设置系统变量

    [root@localhost test]# export AUTHOR=brady [root@localhost test]# echo $AUTHOR brady [root@localhost ...

  7. composer慢 设置阿里云镜像

    composer config -g repo.packagist composer https://mirrors.aliyun.com/composer

  8. py中变量名的“秘密”

    今天突然脑子发抽,想到py里有没有指针这个概念,于是我马上google.baidu了一波,发现网上大多都在说py.java.c#这类纯面向对象的编程语言用对象的概念能完全替代指针.那么问题来了,没有指 ...

  9. Pytest学习(三) - setup和teardown的使用

    一.前言 从文章标题可以看出,就是初始化和释放的操作,根据我的java习惯来学习pytest,个人感觉没差太多,理解上也不是很难. 哦,对了,差点跑题了,这个框架是基于Python语言的,在学习的时候 ...

  10. 【Flutter 混合开发】与原生通信-BasicMessageChannel

    Flutter 混合开发系列 包含如下: 嵌入原生View-Android 嵌入原生View-iOS 与原生通信-MethodChannel 与原生通信-BasicMessageChannel 与原生 ...