[Happy DSA] 2D平面上n個點,求解最接近45度的2點連線

有這麼一個幾何問題:2d平面的n個點,怎麼求解連線最接近45度的那些點的組合。


1. 樸素方法:

我們能想到的最naitive的方法就是求解平面上所有2點連線的斜率,斜率的絕對值最接近於1的那些點的組合,就是我們想要的。要知道n個點能構成n^2條線段,故這種方法的時間複雜度就是O(n^2)。但是我們期望有更好的,時間複雜度較小的解決方法。


2. 一個比較好的方法

如果熟悉計算幾何常用算法的話,類似於這類問題,通常期望更好的時間複雜度會是O(n * lgn)。這裏我們基於該問題的特有屬性,來設計一個時間複雜度是O(n * lgn)的算法。


算法描述:

a. 以原點(0, 0)爲中心,順時針旋轉所有的點到原點的向量45度。這樣所有的點都是在逆時針旋轉45度之後的座標系統中。這個過程可以在O(n)時間內完成。

b. 這樣原始問題可以轉化爲,所有點組成的線段中,求解斜率最接近於0的那些點組成的線段。關於這個問題,我們就比較好的方法了。

I. 對所有的點,以y軸座標進行排序,y軸座標相同的點,以x軸排序。這個過程可以用基本的快排來完成,期待時間會是O(n * lgn)

II. 按照y值兩兩組合的依次遍歷這些點,求解它們的斜率slope=(y2-y1)/(x2-x1)。記錄那些線段的斜率絕對值最小的那些點組合。這個過程可以在O(n)時間內完成。

c. b.II中記錄的那些點組合中點,逆時針旋轉45,可以得到它們原始的點座標。這些點組合就是原始問題中的輸出。


上述算法總的運行時間會是O(n * lgn)。

算法的關鍵點在於爲啥斜率絕對值最小的線段來源於y值連續的兩點。下面從反面來證明下:

假定這兩點是p1(x1, y1), p2(x2, y2), 滿足y1 <= y2。論證如果p1,p2在y值上連續,所有點與p1的連線斜率都將不小於p1p2的連線。從反面來證明。假設p1p2組成的線段的斜率最小,但是y值不連續,這條連線將平面分成了兩部分(上和下)。

1) 存在一個點p3(x3, y3),滿足y1 < y3 < y2。如果p3正好在p1和p2的連線上,那麼斜率是一樣的,我們應該選擇p1p3兩點,或者p3p2,而不是p1p2。得證。

2) 假如p3位於p1p2直線上,滿足y1 < y3 < y2。那麼p3p2組成的直線的斜率要小於p1p2,而且在y值上p3比p1更接近於p2,與前提p1p2斜率最小不符合,得證。

3) 假如p3位於p1p2直線下,滿足y1 < y3 < y2。那麼p1p2組成的直線的斜率要小於p1p2,而且在y值上p3比p2更接近於p1,與前提p1p2斜率最小不符合,得證。

有了上述3個反證,我們可以得知對於一個點來說,斜率最小的直線一定來自於與它y值最鄰近的點。


3. 實現考慮

下面是一個簡單的python語言的實現

#!/usr/bin/env python

import math

def qsort_byY(L):
    if L == []: return []
    return qsort_byY([p for p in L[1:] if p[1] < L[0][1]]) + L[0:1] + \
        qsort_byY([p for p in L[1:] if p[1] >= L[0][1]])

def ccw_rotatept(p, angle):
    angle = angle * math.pi / 180
    x = p[0] * math.cos(angle) - p[1] * math.sin(angle)
    y = p[1] * math.cos(angle) + p[0] * math.sin(angle)
    p[0], p[1] = x, y

def ccw_rotate(L, angle):
    for p in L:
        ccw_rotatept(p, angle)

def solve_closest_45_lines(L):
    print("original points:")
    print(L)
    ccw_rotate(L, -45) # clockwise rotate all points by 45
    print("after rotate")
    print(L)
    qsort_byY(L)
    print("after sort w.r.t. y")
    print(L)

    outp = []
    minslope = 1e100
    for k in range(len(L) - 1):
        i,j = k,k + 1
        curslope = math.fabs((L[j][1] - L[i][1]) \
                           / (L[j][0] - L[i][0]))
        if minslope > curslope:
            del outp[:]
            outp.append([L[i], L[j]])
            minslope = curslope
        elif minslope == curslope:
            outp.append([L[i], L[j]])

    print("output pairs of points:")
    for k in range(len(outp)):
        ccw_rotatept(outp[k][0], 45)
        ccw_rotatept(outp[k][1], 45)
        print outp[k]

if __name__ == "__main__":
    solve_closest_45_lines([[0,0], [10,12],[3,100],[4,102]])

我們發現輸出的點的座標值可能跟最原始有些差距,這是由於rotation數值計算所導致,一種解決方法就是對每個點,存儲額外的一個索引信息,這樣進行運算之後,我們仍然能夠根據index查找到最初那個點的精確座標信息。但是這樣需要對於原始點集合進行一份拷貝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章