跳轉到

3.3 用 SymPy 做解析幾何:直線、距離、交點#

學習動機#

前兩節建立了「點 = 數對、向量 = 序組、距離與角度 = 內積」的座標世界觀。本章開始把這套世界觀工具化:SymPy 的 geometry 模組把解析幾何的物件直接做成可實例化的類——Point、Line、Segment、Polygon——並內建了斜率、距離、交點、面積、平分線等操作。學習的目標不只是「查得到方法」,更是你手上有了一台解析幾何的計算器之後,過去必須靠巧思的題目(三線是否共點、兩線是否垂直、點到線最短路徑在哪)全都變成可以直接呼叫的機械操作。本節將完整走一遍 Line.slope、Line.distance、Line.arbitrary_point、Polygon.area 等核心 API,並說明它們背後的數學來源。

一、直線的四種座標表示#

解析幾何的直線,在歷史上逐步累積出四種典型寫法,各有用途:

名稱 方程 適用情境
點斜式 \(y - y_0 = m(x - x_0)\) 已知一點與斜率
一般式 \(ax + by + c = 0\) 統一處理、SymPy 預設
斜載式 \(y = mx + b\) 函數視角、微積分
向量式 \(P = P_0 + t\,v\) 所有參數化、顯式包含方向

SymPy 的 Line 物件在內部幾乎都是「兩點式」記錄的:Line(p1, p2) 儲存兩個點,其他性質皆由此推導。這個實作的選擇本身就體現解析幾何的根本:兩點定一直線,其餘皆可計算。

1.1 從兩點推一切#

給定 \(A(x_1, y_1)\) 、 \(B(x_2, y_2)\) :

  • 方向向量 \(\vec{v} = (x_2 - x_1,\ y_2 - y_1)\)
  • 斜率 \(m = \dfrac{y_2 - y_1}{x_2 - x_1}\) ( \(x_1 \ne x_2\) )
  • 一般式:由「 \(\vec{v}\) 與 \(P - A\) 垂直」的條件,取法向量 \(n = (-(y_2 - y_1),\ x_2 - x_1)\) ,得
\[ -(y_2 - y_1)(x - x_1) + (x_2 - x_1)(y - y_1) = 0 \]

展開即一般式 \(ax + by + c = 0\) 。注意:直線方程的本質是「點與兩定點組成的向量垂直」——內積思想的第一次實戰應用。

from sympy import symbols, Point, Line, simplify

A = Point(1, 2)
B = Point(4, 8)
l = Line(A, B)

print(l.slope)
# 2

print(l.equation())
# -6*x + 3*y        (即 -6x + 3y = 0,約簡即 y = 2x)

Line.equation() 回傳 \(ax + by + c = 0\) 左端的表達式(SymPy 把常數為 \(0\) 時略去 \(c\) )。這是解析幾何的一般式在 SymPy 中的樣貌。

1.2 垂直與平行#

Line 直接內建判斷與建構:

from sympy import Point, Line, symbols

A, B = Point(1, 2), Point(4, 8)
l = Line(A, B)

# 1. 過給定點的平行線
P = Point(0, 1)
print(l.parallel_line(P).slope)
# 2

# 2. 過給定點的垂直線
print(l.perpendicular_line(P).slope)
# -1/2

# 3. 兩線是否平行 / 垂直
m = Line(Point(0, 0), Point(1, 2))
print(l.is_parallel(m))        # True
print(l.is_perpendicular(Line(Point(0, 0), Point(2, -1))))   # True  (斜率 2 * (-1/2) = -1)

記住兩條性質背後的數學:

  • 平行 \(\iff\) 方向向量成比 \(\iff\) 斜率相等;
  • 垂直 \(\iff\) 方向向量內積為 \(0 \iff m_1 m_2 = -1\) (見 3.2 節)。

二、交點:解方程的幾何#

2.1 兩直線交點#

兩直線

\[ \begin{cases} a_1 x + b_1 y + c_1 = 0 \\ a_2 x + b_2 y + c_2 = 0 \end{cases} \]

的交點就是二元一次方程組的解。解析幾何在這裡與「解 linear system」合流——解不存在則平行,解唯一則相交,解不唯一則共線。這個三態對應,正是把「幾何關係」翻譯成「解空間分類」的第一例,也是線性代數(3.4 節)登場的預告。

from sympy import Point, Line

l1 = Line(Point(0, 0), Point(4, 2))     # y = x/2
l2 = Line(Point(1, 0), Point(1, 5))     # x = 1
print(l1.intersection(l2))
# [Point2D(1, 1/2)]

SymPy 統一用 intersection 處理所有情形,且回傳所有交點(包括圓與圓的兩個交點):

from sympy import Point, Line, Circle

c = Circle(Point(0, 0), 1)
l = Line(Point(-2, 0), Point(2, 0))
print(c.intersection(l))
# [Point2D(-1, 0), Point2D(1, 0)]

# 相切時只有一點:
t = Line(Point(0, 1), Point(2, 1))
print(c.intersection(t))
# [Point2D(0, 1)]

2.2 三線共點:以代數驗證幾何定理#

例(Ceva 定理的簡單情形):三角形 \(A(0,0), B(6,0), C(2,6)\) ,其三條中線是否共點?

from sympy import Point, Line, Segment

A, B, C = Point(0, 0), Point(6, 0), Point(2, 6)
M_bc = Segment(B, C).midpoint   # 中點
M_ca = Segment(C, A).midpoint
M_ab = Segment(A, B).midpoint

med1 = Line(A, M_bc)
med2 = Line(B, M_ca)
med3 = Line(C, M_ab)

p12 = med1.intersection(med2)[0]
print(p12)
# Point2D(8/3, 2)

print(med3.intersection(med1))
# [Point2D(8/3, 2)]        → 三線交於同一點(重心)

在純歐氏幾何中,「三中線共點」需要一整套構造與全等三角形論證;在座標世界,它就是「三個二元方程組有共同解」。解析方法的證明效率由此一覽無遺。

三、距離:內積觀點的集大成#

3.1 點到直線距離#

點 \(P(x_0, y_0)\) 到直線 \(ax + by + c = 0\) 的距離:

\[ d = \frac{|a x_0 + b y_0 + c|}{\sqrt{a^2 + b^2}} \]

公式怎麼來的?兩條路都值得認識:

  1. 最短化路徑:在直線上取參數點 \(Q(t) = P_0 + t\,\vec{v}\) ,把 \(|P - Q(t)|^2\) 當 \(t\) 的二次函數,取其最小值(配方法或微分)。最短距離的 \(Q\) 恰好是 \(P\) 的正交投影。
  2. 內積路徑(上節 2.5): \(P - Q_{\mathrm{proj}}\) 垂直於直線方向,殘差長度即距離。

兩者其實是同一件事的兩種語言。SymPy 的 Line.distance 對應第 1 條路:

from sympy import Point, Line

l = Line(Point(0, 0), Point(3, 0))      # x 軸
P = Point(2, 5)
print(l.distance(P))
# 5

P = Point(2, 5)
l2 = Line(Point(0, 0), Point(3, 4))     # 方程 4x - 3y = 0
print(l2.distance(P))
# 7/5    (|4*2 - 3*5|/5 = 7/5)

3.2 點到線段距離 vs 點到直線距離#

distance 在 Line 與 Segment 上語義不同:對 Segment 而言,若垂足不在線段內,最短距離取到端點的距離。

from sympy import Point, Segment

seg = Segment(Point(0, 0), Point(3, 0))
print(seg.distance(Point(5, 4)))
# sqrt(20) = 2*sqrt(5)      (垂足不在段內,取到端點 (3,0))
seg_l = Segment(Point(-1, 0), Point(10, 0))
print(seg_l.distance(Point(5, 2)))
# 2        (垂足在段內,取垂直距離)

3.3 任意參數點:arbitrary_point#

這是一個結構豐富的方法,值得放在重要位置。Line(p1, p2).arbitrary_point(t) 回傳

\[ \big(\, x_1 + t(x_2 - x_1),\; y_1 + t(y_2 - y_1)\, \big) \]

它不是工具箱中的花邊,而是解析幾何真正的「動態眼睛」:

from sympy import Point, Line, symbols, Rational, simplify

A, B = Point(0, 0), Point(6, 4)
t = symbols('t', real=True)
ln = Line(A, B)
ap = ln.arbitrary_point(t)
print(ap)
# Point2D(6*t, 4*t)

# (1) 把 t 指定為 1/2 → 中點
print(ap.subs(t, Rational(1, 2)))
# Point2D(3, 2)

# (2) 把參數點代入直線方程驗證自動成立
xs, ys = ln.equation().free_symbols
eq = simplify(ln.equation().subs(xs, ap.x).subs(ys, ap.y))
print(eq)
# 0

有了「參數點」,許多幾何條件可以一次翻譯為「 \(t\) 的一次方程」。例如「求直線 \(y = 2x\) 上到 \((4, 3)\) 距離最小的點」——把距離平方表為 \(t\) 的函數、微分(或配方)取最小:

from sympy import Point, Line, symbols, diff, solve, simplify

t = symbols('t', real=True)
l = Line(Point(0, 0), Point(1, 2))
P = Point(4, 3)
ap = l.arbitrary_point(t)                       # (t, 2t)

fsq = simplify(sum([ (ap[i] - P[i])**2 for i in range(2)]))
print(fsq)
# 5*(t - 2)**2 + 5

print(solve(fsq.diff(t), t))
# [2]                    → 最近點 (2, 4)
print(l.distance(P))
# sqrt(5)                → 最短距離。而柱長 |ap - P| 在 t = 2 時 = sqrt(5) ✓
print(Point(2, 4).distance(P))
# sqrt(5)

這一段展示了 3.1 節所說「解析幾何使微積分成為可能」的最小例子:把幾何最佳化問題化為一元函數極值。第 9 章曲線曲率計算,會在同一消息上繼續。

四、多邊形:面積、周長與定向#

4.1 Polygon.area 與鞋帶公式#

Polygon(*points) 支持 \(n\) 多邊形,area 回傳其有向面積(SymPy 對座標排序時可能翻轉符號,取 abs 更穩靠):

from sympy import Point, Polygon, Abs

tri = Polygon(Point(0, 0), Point(5, 0), Point(1, 3))
print(tri.area)
# 15/2

quad = Polygon(Point(0, 0), Point(4, 0), Point(6, 3), Point(1, 3))
print(quad.area)
# 27/2

# 顛倒頂點順序時的「有向性」:
rev = Polygon(Point(1, 3), Point(6, 3), Point(4, 0), Point(0, 0))
print(rev.area)
# -27/2        → 有向面積為負,表示頂點順時針
print(Abs(rev.area))
# 27/2

Polygon.area 的實作原理就是 3.2 節的鞋帶公式——本質是條邊向量叉積。知道臉孔,你就能在 NumPy 中自己實作(8.4 節將會做到)。

4.2 多邊形的角與中線#

from sympy import Point, Polygon, Segment

tri = Polygon(Point(0, 0), Point(6, 0), Point(2, 6))
print(tri.perimeter)
# 6 + 2*sqrt(10) + 2*sqrt(13)      ← 各邊長之和:|AB|=6、|BC|=2√10、|CA|=2√13

print(tri.centroid)
# Point2D(8/3, 2)      ← 與 3.2 節的重心點一致

# 過頂點 (0,0) 的高(字典以各「頂點」為鍵)
alt = tri.altitudes[Point(0, 0)]
print(alt)
# Segment2D(Point2D(0, 0), Point2D(54/13, 36/13))
print(alt.slope)
# 2/3        垂直於對邊 BC(斜率 -3/2):2/3 * (-3/2) = -1 ✓

altitudes、medians、angles 都是「以頂點為鍵」的字典。其中三角形中心族(重心、外心、內心)的座標,SymPy 全部提供:

from sympy import Point, Polygon, simplify

tri = Polygon(Point(0, 0), Point(6, 0), Point(2, 6))
print(tri.centroid)      # Point2D(8/3, 2)     重心
print(tri.circumcenter)  # Point2D(3, 7/3)     外心
inc = tri.incenter
print(simplify(inc.x), simplify(inc.y))
# 6*(1 + sqrt(10))/(3 + sqrt(10) + sqrt(13))   18/(3 + sqrt(10) + sqrt(13))

執行常式驗證:外心 \((3, 7/3)\) 到三頂點距離皆為邊 \(\sqrt{130}/3\) ;內心到三邊距離相等(可用 Segment.distance 檢驗)。這些中心點的座標公式在純幾何中需個別構造證明,在座標世界只是解方程組。順帶一提,「三心」是否重合(即等座標),正是下一節談變換的絕佳練習。

五、綜合範例:五個「經典題家中線」的座標重演#

以下五題都是歷史上「需巧思」的題目;在 SymPy 下每題都是數行呼叫。

5.1 垂直平分線與外接圓#

過 \(P(2, 5)\) 、 \(Q(6, 3)\) 的線段的鉛平分線,就是圓心的候選:

from sympy import Point, Segment, Line, simplify

P, Q = Point(2, 5), Point(6, 3)
M = Segment(P, Q).midpoint
base = Line(P, Q)
perp = base.perpendicular_line(M)          # 過中點的垂直線 = 垂直平分線
print(simplify(perp.slope))
# 2      (PQ 斜率 = (3−5)/(6−2) = −1/2,垂線斜率取負倒數 2)
print(M)
# Point2D(4, 4)

外接圓的圓心是三條垂直平分線之交(三線又共點!)。這與 2.2 的「三中線共點」是同一套計算模板——交點、垂線、中點三個簡單基本的合成。實際上 SymPy 的 Polygon.circumcenter 已經幫你做掉了。

5.2 圓上一點的切線#

半徑 \(OP\) 與切線互相垂直(內積為零)——這是 3.2 節「零內積即垂直」的直接應用。過 \(P\) 的切線就是「過 \(P\) 且垂直於 \(OP\) 」的直線:

from sympy import Point, Line, Circle

c = Circle(Point(0, 0), 5)
P = Point(3, 4)
radius_line = Line(Point(0, 0), P)
tangent = radius_line.perpendicular_line(P)

# 1. 切線性驗證:切線與圓的交點只有 P
print(c.intersection(tangent))
# [Point2D(3, 4)]

# 2. 垂直性驗證
print(radius_line.is_perpendicular(tangent))
# True

5.3 對稱點:鏡射一個點過一條線#

點 \(P\) 對直線 \(l\) 的對稱點:取 \(P\) 的垂足 \(F\) ,則 \(P' = 2F - P\) 。「取垂足」用 projection:

from sympy import Point, Line

P = Point(3, 5)
l = Line(Point(0, 0), Point(1, 0))     # x 軸

F = l.projection(P)
print(F)
# Point2D(3, 0)

Pprime = Point(2*F.x - P.x, 2*F.y - P.y)
print(Pprime)
# Point2D(3, -5)

5.4 點在圓的內外判定#

from sympy import Point, Circle

c = Circle(Point(0, 0), 5)
for q in [Point(3, 4), Point(1, 1), Point(6, 0)]:
    print(q in c, c.center.distance(q))
# True 5
# False sqrt(2)      (在圓內)
# False 6            (在圓外)

注意:in 只判斷「在圓周上」。內外判定要靠距離比較。這是幾何 API 設計與集合語言的差異,在實作中值得留意。

六、過程中你會踩到的事:API 使用要點#

  1. 所有回傳值都是 SymPy 物件:Point2D(1, 1/2) 的 1/2 是 Rational(1,2),不是浮點數。計算嚴格、可符號化——這就是 1.3 節入門強調的「符號幾何」的紅利。
  2. 回傳類型經常是 list:交點是 [Point2D(...), ...]。取第一個用 [0]。
  3. 斜率在鉛直線上是 oo(正無窮):Line(Point(1,0), Point(1,5)).slope == oo 為 True;此時垂直線斜率為 0。程式要對此做分支處理,不要預設斜率是有限數。
  4. 字典型屬性:Polygon.altitudes、medians 等回傳 dict;Polygon.angles 也是。
  5. Point 的加減與乘以純量可用,但 SymPy 幾何類的 Point 與 3.2 節的向量 Matrix 是兩套系統,兩者可互轉(p.x, p.y 或 Matrix([p.x, p.y]))。

七、小結#

方法 用途 背後數學
Line(p1, p2).slope 斜率 \(\Delta y / \Delta x\)
l.equation() 一般式方程 法向量 · 內積條件
l.intersection(m) 交點(list) 解二元一次方程組
l.distance(P) 點到直線距離 正交投影的最小平方
l.arbitrary_point(t) 參數點 參數化曲線(微積分入口)
l.perpendicular_line(P) 垂線 方向向量內積 = 0
Segment.midpoint 中點 平均公式
Polygon.area 面積 鞋帶公式 = 叉積
Polygon.centroid 等 三心 座標平均 / 垂直平分線交點

習題#

習題 3.3-1(直線方程) 用 SymPy 求過 \(A(2, -1)\) 與 \(B(-4, 11)\) 的直線:(1) 斜率;(2) 一般式方程並化為最簡;(3) 在 \(x\) 軸上的截距;(4) 用 l.is_perpendicular 驗證過 \((0, -1)\) 的垂線斜率為 \(-1/2\) 。

習題 3.3-2(三線共點的推廣) 三角形 \(A(0,0), B(8,0), C(2,8)\) 。求:(1) 三條中線交點;(2) 三條高所在直線的交點(垂心);(3) 內角平分線用 SymPy 的 bisectors 或以角度參數構造並驗證三條平分線共點(內心)。三個答案都在三角形內部嗎?

習題 3.3-3(距離與參數點) 過 \(A(1, 1)\) 、 \(B(4, 7)\) 的直線 \(l\) 與 \(P(0, 5)\) :(1) 用 arbitrary_point(t) 將 \(|P - Q(t)|^2\) 表為 \(t\) 的函數;(2) 微分求最小值點 \(Q^*\) ;(3) 驗證 \(Q^* - P\) 與直線方向向量垂直(內積為 0);(4) 用 l.distance(P) 驗證最小距離。

習題 3.3-4(切線與對稱) (1) 圓 \(x^2 + y^2 = 25\) 上點 \((4, 3)\) 的兩個切線?(只有一條)用 perpendicular_line 求、用 intersection 驗證切點唯一;(2) 點 \(P(7, 3)\) 對該圓心「圓的對稱」:對過圓心直線 \(y = x\) 對稱後求對稱點 \(P'\) ,驗證 \(O, P, P'\) 兩兩距離相等。

習題 3.3-5(面積與選點) 四個點 \((0,0), (7, 2), (5, 8), (-2, 4)\) :(1) 用 Polygon.area 算四邊形面積(注意頂點需依序);(2) 把任意 \(Y\) 到 \(X\) 頂點順序亂掉時面積會如何?試一組錯序並解釋絕對值;(3) 用鞋帶公式實作(一道 sum(...) 表達式)驗證,並與 Matrix.det() 版本對比。


下一節:3.4 矩陣與線性變換:幾何的代數化