牛客网-数据结构笔试题目(六)-最近点对问题求解思路
題意
我們先來看下題意吧,題意很簡單,在一個平面當中分布著n個點。現在我們知道這n個點的坐標,要求找出這n個點當中距離最近的兩個點的間距。
分治法
如果我們仔細思考一下,會發現這個問題和排序其實非常類似。因為我們在排序的時候,表面上來看每兩個點之間都存在大小關系,我們要排序似乎也要獲得這些關系。但實際上,我們都知道,無論是快速排序還是歸并排序都可以做到的時間內完成排序。
無論是快速排序還是歸并排序,本質上都是利用的分治法。那么這道題是否也可以使用分治法求解呢?
答案當然是可以的,既然是使用分治法,那么我們首先要做的就是拆分,將整個的數據拆成兩個部分,使用遞歸分別完成兩個部分,然后再合并得到完整的結果。在這個問題當中,我們要拆分數據非常簡單,只需要按照x軸坐標對所有點進行排序,然后選擇中點進行分割即可,分割之后我們得到的結果如下:
拆分結束之后,我們只需要分別統計左邊部分的最近點對、右邊部分的最近點對,以及一個點在左邊一個點在右邊的最近點對即可。對于前面兩種情況都很好解決,我們只需要遞歸就可以搞定了,但對于第三種情況應該怎么辦?這也是本題的難點所在。
求出了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個點作為測試。
接著我們再實現暴力解法,用來檢測我們的算法的正確性,這一段我想應該不用我多說,大家都能理解。
def distance(x, y):return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1])** 2)def brute_force(points):ret = sys.maxsizea, b = None, Nonen = len(points)for i in range(n):for j in range(i+1, n):dis = distance(points[i], points[j])if dis < ret:ret = disa, b = i, jreturn ret, points[a], points[b]最后是重頭戲了,其實算法本身的代碼量并不大,但是其中的細節不少,稍有不慎就可能翻車。
def divide_algorithm(points):n = len(points)# 特判只有一個點或者是兩個點的情況if n < 2:return sys.maxsize, None, Noneelif 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 = dfor u in left:# 左開右閉的二分l, r = -1, len(right)-1while r - l > 1:m = (l + r) >> 1if right[m][1] <= u[1] - d:l = melse:r = midx = r# 在范圍內最多只有6個點能夠構成最近點對for j in range(7):if j + idx >= len(right):breakif distance(u, right[idx + j]) < res:res = distance(u, right[idx + j])a, b = u, right[idx + j]return res, a, b算法是實現完了,但是仍然有一些細節,比如說為什么我們在分治的時候需要先排序再遞歸呢?直接分成兩個部分遞歸行不行?為什么不行?比如我們二分的時候使用的是左閉右開的區間二分?
這兩個問題我先不給出答案,希望大家能夠自己嘗試著思考一下。
總結
以上是生活随笔為你收集整理的牛客网-数据结构笔试题目(六)-最近点对问题求解思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何成为一个合格的算法工程师?这对你来说
- 下一篇: 少儿编程150讲轻松学Scratch(七