有這麼一個幾何問題: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查找到最初那個點的精確座標信息。但是這樣需要對於原始點集合進行一份拷貝。