首發於個人博客 轉載請註明出處python
解析幾何的巔峯
是 向量
那無關過程的狂妄與簡潔
映射着大天然無與倫比的美算法
如何判斷兩條直線是否相交?segmentfault
這很容易。平面直線,無非就是兩種關係:相交 或 平行。所以,只需判斷它們是否平行便可。而直線平行,等價於它們的斜率相等,只需分別計算出它們的斜率,便可作出判斷。spa
但假若我把「直線」換成「線段」呢——如何判斷兩條線段是否相交?code
這就有些難度了。和 直線 不一樣,線段 是有固定長度的,即便它們所屬的兩條直線相交,這兩條線段也不必定相交。blog
也許你會說:分狀況討論不就好了嘛:遊戲
先計算兩條線段的斜率,判斷是否平行。若平行,則必定不相交。ip
若不平行,求出兩條線段的直線方程,聯立之,解出交點座標。rem
運用定比分點公式,判斷交點是否在兩條線段上。get
的確,從理論上這是一個可行的辦法,這也是人們手動計算時廣泛採用的方法。
然而,這個方法並不怎麼適用於計算機。緣由以下:
計算中出現了除法(斜率計算、定比分點),所以每次計算前都要判斷除數是否爲 0(或接近 0)。這很麻煩,嚴重干擾邏輯的表達。
浮點精度丟失帶來的偏差。人類計算時能夠採用分數,但計算機不行。計算機在儲存浮點數時會有精度丟失的現象。一旦算法的計算量大起來,偏差會被急劇放大,影響結果準確性。
效率低下。浮點乘除會十分耗時,不適用於對實時性要求較高的生產環境(如 遊戲)。
那麼,有更好的方法?
固然有。
本文的算法將用 python 描述,主要用到兩個數據類型:
# 點 class Point(object): def __init__(self, x, y): self.x, self.y = x, y # 向量 class Vector(object): def __init__(self, start_point, end_point): self.start, self.end = start_point, end_point self.x = end_point.x - start_point.x self.y = end_point.y - start_point.y
先在此處說明。
對於「判斷兩條直線是否相交」這個問題,咱們之因此能迅速而準確地進行判斷,是由於「相交」與「不相交」這兩個狀態有着明顯的不一樣點,即 斜率是否相等。
那麼如今,爲了判斷兩條線段是否相交,咱們也要找出「相交」與「不相交」這兩個狀態的不一樣點。
假設如今有兩條線段 AB 和 CD,咱們畫出它們之間的三種關係:
其中,狀況 1 爲不相交,狀況 二、3 爲相交。
做出向量 AC、AD、BC、BD。
首先介紹一個概念: 向量有序對的旋轉方向。這個概念指:對於共起點有序向量二元組(a, b)
,其旋轉方向爲 使 a 可以旋轉一個小於 180 度的角並與 b 重合的方向,簡記爲 direct(a, b)
。若 a
和 b
反向共線,則旋轉方向取任意值。
舉個例子:圖一中,direct(AC, AD)
爲順時針方向。
接下來咱們要分析四個值:direct(AC, AD)
、direct(BC, BD)
、direct(CA, CB)
、direct(DA, DB)
。
對於圖一,direct(AC, AD)
和 direct(BC, BD)
都爲順時針,direct(CA, CB)
爲逆時針,direct(DA, DB)
爲順時針。
對於圖二,direct(AC, AD)
爲順時針,direct(BC, BD)
爲任意方向,direct(CA, CB)
爲逆時針,direct(DA, DB)
爲順時針。
對於圖三,direct(AC, AD)
、direct(DA, DB)
爲順時針,direct(BC, BD)
、direct(CA, CB)
爲逆時針。
不難發現,兩條線段相交的充要條件是:direct(AC, AD) != direct(BC, BD)
且 direct(CA, CB) != direct(DA, DB)
。這即是「相交」與「不相交」這兩個狀態的不一樣點。
然而你可能會以爲:旋轉方向這麼一個虛無飄渺的東西,怎麼用程序去描述啊?
再來看一幅圖:
再來定義有向角:
有向角
<a, b>
爲 向量a
逆時針 旋轉到與 向量b
重合所通過的角度。
不難看出,對於向量a
、b
:
若 direct(a, b)
爲逆時針,則 0 <= <a, b> <= 180
,從而 sin<a, b> >= 0
。
若 direct(a, b)
爲順時針,則 180 <= <a, b> <= 360
,從而 sin<a, b> <= 0
。
這樣一來,咱們能夠將旋轉方向的問題轉化爲 求有向角正弦值 的問題。而這個問題,是很容易的。
如上圖,記
$$ OA = (x_1, y_1), OB = (x_2, y_2) $$
$$ |OA| = r_1, |OB| = r_2 $$
則
$$ sin(\lt OA, OB\gt) $$
$$ = sin \theta $$
$$ = sin (\alpha - \beta) $$
$$ = sin \alpha cos \beta - sin \beta cos \alpha $$
$$ = \frac{(sin \alpha cos \beta - sin \beta cos \alpha) \cdot r_1 \cdot r_2}{r_1 \cdot r_2} $$
$$ = \frac{x_1 \cdot y_2 - x_2 \cdot y_1} {r_1 \cdot r_2} $$
而這裏,咱們要的只是 sin(<OA, OB>)
的符號,而 r1
和 r2
又都是恆正的,所以只需判斷 x1 * y2 - x2 * y1
的符號便可。
這個方法的數學背景是 叉乘,能夠前往 Wikipedia 瞭解更多。
由點 A,B,C,D 計算出向量 AC,AD,BC,BD
計算 sin(<AC, AD>) * sin(<BC, BD>)
和 sin(<CA, CB>) * sin(<DA, DB>)
,若皆爲非正數,則相交;不然,不相交。
終於到代碼部分了,想必你們都已不耐煩了吧。
在向量的輔助下,代碼顯得異常簡單。
ZERO = 1e-9 def negative(vector): """取反""" return Vector(vector.end_point, vector.start_point) def vector_product(vectorA, vectorB): '''計算 x_1 * y_2 - x_2 * y_1''' return vectorA.x * vectorB.y - vectorB.x * vectorA.y def is_intersected(A, B, C, D): '''A, B, C, D 爲 Point 類型''' AC = Vector(A, C) AD = Vector(A, D) BC = Vector(B, C) BD = Vector(B, D) CA = negative(AC) CB = negative(BC) DA = negative(AD) DB = negative(BD) return (vector_product(AC, AD) * vector_product(BC, BD) <= ZERO) \ and (vector_product(CA, CB) * vector_product(DA, DB) <= ZERO)
一鼓作氣,沒有惱人的除法,沒有狀況討論,只是純粹的簡單運算。