94·数学算法进阶

计算几何基础

geometrypointlinecross-product

第 94 课 - 计算几何基础

1. 学习目标

通过本节课的学习,你将能够:

  • 理解点、向量、线段在计算机中的标准表示方法。
  • 掌握向量的点积和叉积这两种核心运算的定义与实现。
  • 理解叉积的几何意义,并利用其符号判断向量的相对方向(顺时针、逆时针、共线)。
  • 应用叉积解决两个基础几何问题:判断点在线段的哪一侧,以及判断两条线段是否相交。

2. 核心概念

在计算几何中,我们通常将几何对象转化为数字进行精确计算。一切始于点和向量。

2.1 点与向量的表示

  • 点 (Point):我们用一个坐标 (x, y) 来表示平面上的一个点。
  • 向量 (Vector):向量表示一个具有大小和方向的量。在几何上,它常用来表示从点A到点B的位移。向量可以由两个点相减得到:向量 AB = (B.x - A.x, B.y - A.y)。注意,向量只有方向和长度,没有固定的位置。

2.2 向量的基本运算

  • 加减:对应分量相加减。例如 (a, b) + (c, d) = (a+c, b+d)
  • 标量乘法k * (x, y) = (k*x, k*y)
  • 点积 (Dot Product)a · b = a.x * b.x + a.y * b.y,结果是一个标量。点积的几何意义是投影,可用于判断向量是否垂直(点积为0)。
  • 叉积 (Cross Product):这是计算几何中最核心的运算。对于二维向量 a = (a.x, a.y)b = (b.x, b.y),它们的叉积是一个标量(在三维中是向量): a × b = a.x * b.y - a.y * b.x

2.3 叉积的几何意义与应用

叉积的绝对值等于以这两个向量为邻边的平行四边形的面积。 更重要的是叉积的符号

  • a × b > 0:向量b位于向量a的逆时针方向(左侧)。
  • a × b < 0:向量b位于向量a的顺时针方向(右侧)。
  • a × b == 0:向量a与向量b共线(平行或反向)。

这个性质是我们判断方向和位置关系的基石。例如,给定三个点A、B、C,我们可以计算 向量AB向量AC 的叉积,来判断C点相对于线段AB的位置。

3. 代码示例

import math

class Point:
    """表示一个二维平面上的点"""
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    # 定义点的减法,用于生成向量
    def __sub__(self, other):
        return Point(self.x - other.x, self.y - other.y)

    def __repr__(self):
        return f"({self.x}, {self.y})"

class Vector:
    """表示一个二维向量"""
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def from_points(cls, p1, p2):
        """从两个点创建向量"""
        return cls(p2.x - p1.x, p2.y - p1.y)

    # 向量加法
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    # 向量减法
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    # 标量乘法
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    # 计算向量的模(长度)
    def magnitude(self):
        return math.sqrt(self.x**2 + self.y**2)

    # 计算点积
    def dot(self, other):
        return self.x * other.x + self.y * other.y

    # 计算叉积
    def cross(self, other):
        return self.x * other.y - self.y * other.x

    def __repr__(self):
        return f"<{self.x}, {self.y}>"

def cross_product_sign(p, q, r):
    """计算向量 pq 和 pr 的叉积符号,用于判断方向。
       返回值: 1(逆时针), -1(顺时针), 0(共线)"""
    vec_pq = Vector.from_points(p, q)
    vec_pr = Vector.from_points(p, r)
    cp = vec_pq.cross(vec_pr)
    if cp > 0:
        return 1  # 逆时针
    elif cp < 0:
        return -1 # 顺时针
    else:
        return 0  # 共线

def segments_intersect(p1, p2, p3, p4):
    """判断线段(p1,p2)和线段(p3,p4)是否相交。
       使用叉积法:需要满足两个条件。"""
    # 条件1: 线段p1p2的端点p3, p4应在其两侧。
    d1 = cross_product_sign(p1, p2, p3)
    d2 = cross_product_sign(p1, p2, p4)

    # 条件2: 线段p3p4的端点p1, p2应在其两侧。
    d3 = cross_product_sign(p3, p4, p1)
    d4 = cross_product_sign(p3, p4, p2)

    # 一般情况:每个线段的两个端点分别在另一线段的两侧。
    if (d1 * d2 < 0) and (d3 * d4 < 0):
        return True

    # 特殊情况:处理三点共线且重叠的情况(简化版,实际竞赛需更严谨)
    # 这里仅演示思路,不包含所有边界情况。
    # 可检查点是否在对方线段上(使用点积判断投影是否在线段内)
    # 完整的算法需要考虑更多边界情况。
    return False

# --- 测试代码 ---
if __name__ == "__main__":
    # 创建点
    A = Point(0, 0)
    B = Point(4, 0)
    C = Point(2, 3)
    D = Point(2, -1)

    # 测试叉积符号 (方向判断)
    print(f"点 C(2,3) 相对于线段 AB(0,0)->(4,0) 的位置: ", end="")
    direction = cross_product_sign(A, B, C)
    if direction == 1:
        print("左侧 (逆时针)")
    elif direction == -1:
        print("右侧 (顺时针)")
    else:
        print("在线上 (共线)")

    print(f"点 D(2,-1) 相对于线段 AB(0,0)->(4,0) 的位置: ", end="")
    direction = cross_product_sign(A, B, D)
    if direction == 1:
        print("左侧 (逆时针)")
    elif direction == -1:
        print("右侧 (顺时针)")
    else:
        print("在线上 (共线)")

    # 测试线段相交
    P1, P2 = Point(0, 0), Point(2, 2)
    P3, P4 = Point(0, 2), Point(2, 0)
    print(f"\n线段 P1P2(0,0)-(2,2) 和 线段 P3P4(0,2)-(2,0) 是否相交? {segments_intersect(P1, P2, P3, P4)}")

    P1, P2 = Point(0, 0), Point(1, 1)
    P3, P4 = Point(1, 0), Point(2, 1)
    print(f"线段 P1P2(0,0)-(1,1) 和 线段 P3P4(1,0)-(2,1) 是否相交? {segments_intersect(P1, P2, P3, P4)}")

4. 实践练习

练习 1:基础叉积计算

给定向量 u = (1, 2)v = (3, 1),手动计算或编写代码求出它们的点积和叉积,并根据叉积的符号判断 v 相对于 u 的方向。 预期输出

点积: 5
叉积: -5
v 相对于 u 的方向: 顺时针 (右侧)

练习 2:判断三点构成的转向

编写一个函数 turn_direction(A, B, C),输入三个点,返回从A->B->C是左转、右转还是直行。 要求:使用叉积实现。 示例输入与输出

print(turn_direction(Point(0,0), Point(1,1), Point(2,0))) # 应输出 “右转”
print(turn_direction(Point(0,0), Point(1,1), Point(0,2))) # 应输出 “左转”
print(turn_direction(Point(0,0), Point(2,2), Point(1,1))) # 应输出 “共线”

练习 3:点是否在三角形内(选做)

给定一个三角形的三个顶点 A, B, C 和一个测试点 P,判断点 P 是否在三角形 ABC 的内部(包括边界)。 思路:可以利用叉积判断点P是否同时在三条边AB、BC、CA的同一侧(例如,都在左侧或都在右侧)。 示例A(0,0), B(4,0), C(0,4), P(1,1) 应在三角形内;P(3,3) 应在三角形外。

5. 常见错误

  1. 混淆点积与叉积:点积用于判断垂直或计算投影,结果是标量;叉积用于判断方向或计算面积,结果是带符号的标量(二维)。
  2. 误解叉积为零的含义:叉积为零只能说明两个向量共线(方向相同或相反),但不能直接推出点共线。要判断三点共线,需计算两个向量的叉积是否为零。
  3. 忽略叉积的符号:只关注叉积的大小(绝对值)而忽略其符号,会丢失最重要的方向信息。
  4. 线段相交判断不完整:仅检查 d1 * d2 < 0d3 * d4 < 0一般情况。当有共线点时,这个判断会失效。完整的算法还需要处理端点相交、一条线段端点落在另一条线段上等退化情况,通常需要检查点是否在线段上(点积法)。
  5. 坐标系问题:在标准的数学坐标系(y轴向上)中,叉积符号的意义如前所述。但在计算机图形学常用的屏幕坐标系(y轴向下)中,左右关系可能会反过来,需要根据具体环境调整。

6. 小结

本节课我们学习了计算几何的入门知识:

  • 基础表示:点用坐标(x, y)表示,向量用分量差表示。
  • 核心运算叉积 (a.x*b.y - a.y*b.x) 是计算几何的灵魂,其符号揭示了两个向量的相对方向(逆时针/顺时针/共线)。
  • 方向判断:利用三个点构成的两个向量的叉积符号,可以判断转向。
  • 线段相交:利用叉积判断两个线段的端点是否分别在另一条线段的两侧,是判断线段相交的基本方法(需注意退化情况)。 这些概念是解决更复杂几何问题(如下一节课的凸包)的基石。熟练掌握叉积的应用是成为计算几何高手的第一步。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「凸包」 以巩固所学知识。