用SymPy自动求解三角形构造与全等条件验证

Manim 动画演示三角形全等判定定理时,我需要根据给定的边长或角度条件,在坐标系中精确放置三角形的三个顶点。

手动调整点的位置来凑 SSSSAS 这些条件,反复试错、坐标对不齐,根本没法精确展示"给定条件后三角形唯一确定"这个核心结论。

这篇文章用 SymPy 把几何约束转化为代数方程,自动解出顶点坐标,让动画精准且高效。

1. 痛点场景还原

假设我要做一个 SSS 全等判定的演示:给定三边长度 AB=4, BC=3, AC=5,在坐标系中画出这个三角形,然后改变边长观察三角形是否唯一确定。

如果纯手动操作,我会这样写:

python 复制代码
from manim import *
import numpy as np


class PainfulSSSDemo(Scene):
    def construct(self):
        # 手动指定顶点坐标 ------ 怎么知道这三个坐标能满足边长条件?
        A = np.array([0, 0, 0])  # 固定 A 在原点
        B = np.array([4, 0, 0])  # 固定 B 在 (4,0),这样 AB=4
        # C 的位置?需要同时满足 AC=5, BC=3
        # 只能手动解方程:x²+y²=25, (x-4)²+y²=9
        # 手算得 x=4, y=3,于是 C=(4,3) ------ 但这是直角三角形特例
        # 换一组边长又要重新手算,而且每次都要判断镜像解
        C = np.array([4, 3, 0])

        triangle = Polygon(A, B, C)
        self.add(triangle)

核心痛点:

  • 给 A、B 定好位置后,C 的坐标必须同时满足 AC 和 BC 的距离约束,手算就是解二元二次方程组,换个边长就要重算一次。
  • SASASA 更麻烦,涉及角度条件,需要把" \\angle A = 60\^{\\circ} "转化为向量点积方程,手算极易出错。
  • 方程组通常有两组解(镜像三角形),需要判断哪个是合理的朝向。
  • 手动算出来的坐标是近似值,动画中顶点位置不够精准。

这些计算本质上就是几何约束求解 ,完全可以交给 SymPy 自动完成。

2. SymPy 解决方案介绍

SymPy 可以将几何条件转化为代数方程,然后自动求解顶点坐标。

2.1 SSS 全等:已知三边求顶点

已知 A=(0,0) , B=(c,0) ,求 C=(x,y) 满足 AC=b , BC=a

python 复制代码
import sympy as sp

x, y = sp.symbols('x y', real=True)
a, b, c = 3, 5, 4  # BC, AC, AB

# 距离约束转化为方程
eq1 = (x - 0)**2 + (y - 0)**2 - b**2  # AC = 5
eq2 = (x - c)**2 + (y - 0)**2 - a**2  # BC = 3

solutions = sp.solve([eq1, eq2], (x, y))
# 输出两组解:[(4, -3), (4, 3)]  ------ 对应镜像三角形

对于 SASASA,只需把角度条件用向量点积或余弦定理表示,同样构建方程组求解。

2.2 SAS 全等:已知两边和夹角求第三顶点

已知 AB=c , AC=b , \\angle A=\\theta \\(,\\) A 在原点, B (c,0) \\(,\\) C 满足到 A 的距离为 b 、到 B 的距离用余弦定理求:

python 复制代码
import sympy as sp

x, y = sp.symbols('x y', real=True)
b, c, theta = 4, 5, sp.rad(60)  # AC=4, AB=5, ∠A=60°

# C 到 A 的距离
eq1 = x**2 + y**2 - b**2
# 用余弦定理:BC² = AB² + AC² - 2·AB·AC·cosθ
bc_sq = c**2 + b**2 - 2*c*b*sp.cos(theta)
eq2 = (x - c)**2 + y**2 - bc_sq

solutions = sp.solve([eq1, eq2], (x, y))

2.3 筛选合理解

SymPy 会返回两组解(关于 AB 所在直线对称),通过指定 y 的正负号可以筛选:

python 复制代码
# 筛选 y >= 0 的解(取上方三角形)
valid_solution = [sol for sol in solutions if sol[1] >= 0][0]

这样就能得到唯一确定的三角形顶点坐标,完美支撑全等判定定理的可视化。

3. Manim 联动实战

下面是一个完整的动画场景,用 ValueTracker 控制边长,动态展示 SSS 全等下三角形的唯一确定性。

python 复制代码
from manim import *
import sympy as sp
import numpy as np


class SSSCongruenceDemo(Scene):
    def construct(self):
        # 固定两个顶点
        A = np.array([-1, 0, 0])
        B = np.array([1, 0, 0])

        # 可调边长
        a_tracker = ValueTracker(2)  # BC
        b_tracker = ValueTracker(2)  # AC

        # 用 always_redraw 动态更新三角形(两个镜像三角形)
        triangles = always_redraw(
            lambda: self.get_triangles(
                A, B, a_tracker.get_value(), b_tracker.get_value()
            )
        )
        self.add(triangles)

        # 顶点标签
        labels = always_redraw(
            lambda: self.get_labels(A, B, a_tracker.get_value(), b_tracker.get_value())
        )
        self.add(labels)

        # 边长标注
        side_labels = always_redraw(
            lambda: self.get_side_labels(
                A, B, a_tracker.get_value(), b_tracker.get_value()
            )
        )
        self.add(side_labels)

        # 动画:改变 BC 和 AC 的长度
        self.play(a_tracker.animate.set_value(3), run_time=2)
        self.play(b_tracker.animate.set_value(1), run_time=2)
        self.play(
            a_tracker.animate.set_value(2), b_tracker.animate.set_value(2), run_time=2
        )
        self.wait()

    def solve_vertex_C(self, A, B, a, b):
        """用 SymPy 求解顶点 C,返回两个镜像点的 np.array 坐标列表"""
        x, y = sp.symbols("x y", real=True)
        # AB 的长度
        c = np.linalg.norm(B - A)

        # 距离约束方程
        eq1 = (x - 0) ** 2 + (y - 0) ** 2 - b**2  # 以 A 为原点
        eq2 = (x - c) ** 2 + (y - 0) ** 2 - a**2  # 以 B 为原点

        solutions = sp.solve([eq1, eq2], (x, y), dict=True)
        if not solutions:
            return []

        # 转换到实际坐标系
        AB_vec = B - A
        x_axis = AB_vec / c
        y_axis = np.array([-x_axis[1], x_axis[0], 0])

        C_points = []
        for sol in solutions:
            sol_x = float(sp.N(sol[x]))
            sol_y = float(sp.N(sol[y]))
            # 局部坐标转全局坐标
            C = A + sol_x * x_axis + sol_y * y_axis
            C_points.append(C)
        return C_points

    def get_triangles(self, A, B, a, b):
        C_points = self.solve_vertex_C(A, B, a, b)
        if not C_points:
            return VGroup()  # 无法构成三角形时返回空

        triangles = VGroup()
        colors = [BLUE, GREEN]
        for i, C in enumerate(C_points):
            triangle = Polygon(
                A, B, C, color=colors[i % 2], fill_opacity=0.3, stroke_width=2
            )
            triangles.add(triangle)
        return triangles

    def get_labels(self, A, B, a, b):
        C_points = self.solve_vertex_C(A, B, a, b)
        if not C_points:
            return VGroup()

        label_A = MathTex("A", color=WHITE, font_size=28).next_to(A, DL, buff=0.15)
        label_B = MathTex("B", color=WHITE, font_size=28).next_to(B, DR, buff=0.15)

        labels = VGroup(label_A, label_B)
        for i, C in enumerate(C_points):
            direction = UP if C[1] >= A[1] else DOWN
            label_C = MathTex(f"C_{i+1}", color=WHITE, font_size=28).next_to(
                C, direction, buff=0.15
            )
            labels.add(label_C)
        return labels

    def get_side_labels(self, A, B, a, b):
        C_points = self.solve_vertex_C(A, B, a, b)
        if not C_points:
            return VGroup()

        c = np.linalg.norm(B - A)
        labels = VGroup()

        # AB 边长标注(共用)
        label_AB = MathTex(f"{c:.1f}", font_size=24, color=YELLOW).move_to(
            (A + B) / 2 + DOWN * 0.3
        )
        labels.add(label_AB)

        # 每个三角形的 AC 和 BC 边长标注
        for i, C in enumerate(C_points):
            direction = LEFT if C[0] < (A[0] + B[0]) / 2 else RIGHT
            label_AC = MathTex(f"{b:.1f}", font_size=24, color=RED).move_to(
                (A + C) / 2 + direction * 0.3
            )
            label_BC = MathTex(f"{a:.1f}", font_size=24, color=RED).move_to(
                (B + C) / 2 + (-direction) * 0.3
            )
            labels.add(label_AC, label_BC)

        return labels

关键点解释:

  • solve_vertex_C 是核心:将 A、B 固定后,以 A 为原点、AB 为 x 轴建立局部坐标系,用 SymPy 解二元二次方程组求 C 的局部坐标,再通过向量旋转平移到全局坐标系。
  • 同时显示 C 点两个解的镜像,上下两个全等三角形在视觉上稳定一致。
  • always_redraw 保证边长改变时三角形实时更新,顶点坐标由 SymPy 自动重新计算,完全不用手动干预。
  • 当给定三边长度不满足三角形不等式时,solve 无实解,函数返回 None,动画自动隐藏三角形,自然展示了"不是任意三边都能构成三角形"。

4. 效果展示说明

运行这个场景,你会看到:

  • 坐标系中, A B 两个顶点固定不变,边长标注清晰显示当前的 AB \\(、\\) BC \\(、\\) AC 长度。
  • **改变 ** BC \\(\*\* 的长度\*\*:\\) C 点沿一条弧线移动,三角形的形状随之变化,但始终满足给定的三边长度约束。你能直观看到"改变一边,三角形形状唯一确定"。
  • **改变 ** AC \\(\*\* 的长度\*\*:类似地,\\) C 点在另一条弧线上移动,三角形的三个顶点自动重新定位。
  • 同时改变两边:三角形平滑过渡到新的形状,整个过程顶点坐标精确、无任何手动调整的痕迹。
  • 如果输入的边长无法构成三角形(如 a+b \\leqslant c ),三角形消失,自然展示了三角形不等式这个隐含条件。

以此为基础,只需修改方程组,就可以扩展为 SASASAAAS 等其他全等判定的可视化演示。

5. 小结

SymPyManim 几何动画中的作用可以概括为:将几何约束转化为代数方程,让计算机解出精确的顶点坐标。

你不再需要手动凑坐标、手算方程组、处理镜像解,只需要描述"已知条件是什么",SymPy 就会返回"点应该在哪里"。

这个思路不仅适用于全等三角形,也适用于任何需要精确几何构造的场景:尺规作图、动点轨迹、最值问题等等。

把数学计算交给 SymPy,把视觉表达留给 Manim,两者配合才能做出真正精准而优雅的数学动画。

相关推荐
wang_yb3 天前
用SymPy自动计算抛物线求根、判别式与顶点
databook·manim
wang_yb5 天前
一次函数图像工厂:用 SymPy 自动生成 y=kx+b 对比动画
databook·manim
wang_yb7 天前
用 SymPy 解决 Manim 曲线绘制速度不均的问题
databook·manim
wang_yb14 天前
Manim物理模拟:别自己写欧拉了!
databook·manim
wang_yb16 天前
轨迹的蓝图:方程求解与交点计算
databook·manim
wang_yb18 天前
填充与积累:积分与面积的可视化
databook·manim
wang_yb20 天前
切线的魔法:用 SymPy 和 Manim 轻松搞定导数动画
databook·manim
wang_yb22 天前
让数学公式自动推导
databook·manim
wang_yb23 天前
告别手动计算,SymPy 初识与 Manim 联动
databook·manim