數學美 之 判斷線段相交的最簡方法

首發於個人博客 轉載請註明出處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. 不相交

  2. 交點位於某條線段上

  3. 相交

其中,狀況 1 爲不相交,狀況 二、3 爲相交。

做出向量 AC、AD、BC、BD。

首先介紹一個概念: 向量有序對的旋轉方向。這個概念指:對於共起點有序向量二元組(a, b),其旋轉方向爲 使 a 可以旋轉一個小於 180 度的角並與 b 重合的方向,簡記爲 direct(a, b)。若 ab 反向共線,則旋轉方向取任意值。

舉個例子:圖一中,direct(AC, AD) 爲順時針方向。

接下來咱們要分析四個值:direct(AC, AD)direct(BC, BD)direct(CA, CB)direct(DA, DB)

  1. 對於圖一,direct(AC, AD)direct(BC, BD) 都爲順時針,direct(CA, CB) 爲逆時針,direct(DA, DB) 爲順時針。

  2. 對於圖二,direct(AC, AD) 爲順時針,direct(BC, BD) 爲任意方向,direct(CA, CB) 爲逆時針,direct(DA, DB) 爲順時針。

  3. 對於圖三,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 重合所通過的角度。

不難看出,對於向量ab

  • 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>) 的符號,而 r1r2 又都是恆正的,所以只需判斷 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)

一鼓作氣,沒有惱人的除法,沒有狀況討論,只是純粹的簡單運算。

相關文章
相關標籤/搜索