用SymPy自动求解追及问题的方程

做追及问题动画时,需要根据题意列方程求出相遇时间,再手动计算两个物体在每个时刻的坐标。

题意中速度、初始距离、出发时间差这些参数一改变,就得重新手算一遍,整个过程繁琐且易错。

本文用 SymPy 把列方程和求解都自动化,直接得到相遇时间和运动轨迹,动画代码只需拿到结果去画图。

1. 痛点场景还原

假设做一个最经典的追及动画:甲从原点出发,速度 $ v_1=2 $;乙从 $ x=10 $ 处同向出发,速度 $ v_2=5 $,问多久追上。

如果用纯手工方式写 Manim

class PainfulCatchUp(Scene):
    def construct(self):
        # 手动列方程并求解
        # 设 t 为乙出发后的时间,甲的位置:2*(t+?),乙的位置:10+5*t
        # 如果同时出发:2t = 10+5t → t = -10/3 负数无意义
        # 改甲先出发2秒:2(t+2) = 10+5t → 2t+4=10+5t → -3t=6 → t=-2 还是负
        # 必须反复调整题设,手算满足实际情况的初始条件
        # 这里干脆让乙追甲,甲在乙前面:
        # 甲在x=10以v=2向前,乙在x=0以v=5同时出发 → 5t = 10+2t → 3t=10 → t=10/3
        v1, v2 = 2, 5
        t_meet = 10/3   # 手动解出的结果
        meet_x = 5 * t_meet  # 再手动算相遇位置

        # 甲和乙的轨迹只能硬编码
        def pos1(t):
            return 10 + v1 * t
        def pos2(t):
            return v2 * t

        # 然后创建动画……

痛点很明显:

  • 每次改变速度或初始距离,都要重新手写方程、求解、算相遇坐标。
  • 题目条件稍微变化(比如“甲先走1分钟”、“乙在中途休息”),手算的过程就得全部推翻重来。
  • 容易在单位换算、正方向等细节上出错,动画一旦跑起来发现不对,排查起来也费劲。

这些计算本质上就是根据文字描述建立代数方程并求解,正是 SymPy 最擅长的事。

2. SymPy 解决方案介绍

SymPy 可以让我们用符号把追及问题“翻译”成方程,然后自动求解。

import sympy as sp

# 符号定义:t 为乙出发后经过的时间
t = sp.symbols('t', positive=True)
v1, v2 = 2, 5          # 速度
s0 = 10                # 初始距离(甲在乙前面10米)

# 甲的位置:先出发0秒(即同时出发),位置 = s0 + v1*t
pos1 = s0 + v1 * t
# 乙的位置:从0开始,位置 = v2*t
pos2 = v2 * t

# 相遇条件:位置相等
eq = sp.Eq(pos1, pos2)
solution = sp.solve(eq, t)
# 输出: [10/3]

如果甲先出发 2 秒,方程只需改一下:

t_delay = 2  # 甲早出发2秒
pos1 = s0 + v1 * (t + t_delay)   # 甲多走2秒
eq = sp.Eq(pos1, pos2)
solution = sp.solve(eq, t)
# 输出: [14/3]

无论怎么变化,我们只需要修改符号表达式的构建逻辑,求解交给 solve,相遇坐标直接代入即可。

接下来把这个思想嵌入 Manim,动画就能自适应任意追及条件。

3. Manim 联动实战

下面是一个完整的动画场景:给定甲、乙的初始位置、速度和出发延迟,自动计算相遇点,并动态展示追及过程。

from manim import *
import sympy as sp


class CatchUpLab(Scene):
    def construct(self):
        # ========== 题目参数(任意修改这里即可) ==========
        v1 = 1.5  # 甲的速度
        v2 = 2.5  # 乙的速度
        init_gap = 8  # 初始距离(甲在乙前面)
        delay = 1  # 甲早出发的时间

        # ========== SymPy 自动求解 ==========
        t = sp.symbols("t", positive=True)
        pos1_expr = init_gap + v1 * (t + delay)  # 甲的位置
        pos2_expr = v2 * t  # 乙的位置
        eq = sp.Eq(pos1_expr, pos2_expr)
        t_meet = sp.solve(eq, t)[0]  # 精确解
        meet_x = float(pos2_expr.subs(t, t_meet))  # 相遇位置

        # 为了动画流畅,预先计算两个运动函数(可以直接用lambda)
        def pos1_func(time):
            return init_gap + v1 * (time + delay)

        def pos2_func(time):
            return v2 * time

        # ========== 场景搭建 ==========
        axes = NumberLine(
            x_range=[0, 30, 5],
            length=8,
            include_numbers=True,
            label_direction=DOWN,
        )
        self.play(Create(axes))

        # 甲和乙的点
        dot1 = Dot(color=RED, radius=0.2)
        dot2 = Dot(color=BLUE, radius=0.2)
        # 初始放置
        dot1.move_to(axes.number_to_point(pos1_func(0)))
        dot2.move_to(axes.number_to_point(pos2_func(0)))
        self.add(dot1, dot2)

        # 标签
        label1 = Text("甲", color=RED).next_to(dot1, UP * 2)
        label2 = Text("乙", color=BLUE).next_to(dot2, UP * 2)
        self.add(label1, label2)

        # 轨迹虚线(预留)
        trace1 = TracedPath(dot1.get_center, stroke_color=RED, stroke_width=2)
        trace2 = TracedPath(dot2.get_center, stroke_color=BLUE, stroke_width=2)
        self.add(trace1, trace2)

        # 相遇点标记(先隐藏,等追到时再显示)
        meet_dot = Dot(point=axes.number_to_point(meet_x), color=YELLOW)
        meet_label = Text(f"相遇点: {meet_x:.2f}", font_size=24, color=YELLOW)
        meet_label.next_to(meet_dot, UP * 1.5)

        # 动态更新的时间显示
        time_text = MathTex("t=0.0").shift(UL * 2)
        self.add(time_text)

        # 追击动画
        total_time = float(t_meet) + 2  # 多跑2秒

        def update_dots(mob, alpha):
            # alpha 从0到1,对应时间从0到total_time
            t_now = alpha * total_time
            dot1.move_to(axes.number_to_point(pos1_func(t_now)))
            dot2.move_to(axes.number_to_point(pos2_func(t_now)))
            # 更新标签位置
            label1.next_to(dot1, UP * 2)
            label2.next_to(dot2, UP * 2)
            # 更新时间显示
            time_text.become(MathTex(f"t={t_now:.1f}").shift(UL * 2))
            # 判断是否到达相遇点
            if t_now >= float(t_meet):
                self.add(meet_dot, meet_label)  # 显示相遇标记

        self.play(
            UpdateFromAlphaFunc(
                VGroup(dot1, dot2, label1, label2, time_text),
                update_dots,
                run_time=total_time,
                rate_func=linear,
            )
        )
        self.wait(1)

关键点解释:

  • 参数集中在开头,修改 v1, v2, init_gap, delay 就能改变整个题目,SymPy 会自动重新求解相遇时间和位置,动画完全自适应。
  • UpdateFromAlphaFunc 驱动动画,每一帧根据当前时间计算两个点的坐标,而坐标函数是直接用 SymPy 解出的表达式生成的,精确无误差。
  • 相遇点预先用 sp.solve 得到精确值,当动画时间超过相遇时刻时,黄色标记出现,直观展示“甲被乙追上”的瞬间。
  • 没有使用 always_redraw 而采用 UpdateFromAlphaFunc,是为了更好地控制动画进度和时间显示,同时避免复杂的依赖更新。

4. 效果展示说明

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

  • 一条水平数轴,红点(甲)在蓝点(乙)的前方。
  • 动画开始后,两点同时向右移动,蓝点速度更快,逐渐逼近红点。
  • 在精确的相遇时刻,一个黄色圆点出现在相遇位置,并标注坐标,同时甲和乙重合。
  • 即使改变参数——比如甲的速度从 1.5 改成 1.2、初始距离改成 12、甲提前出发 3 秒,只需修改脚本顶部的几个数字,无需任何手动计算,动画仍然能够准确呈现追及过程,并自动在正确的位置标记相遇点。
  • 如果参数设置使得无法追上(如乙的速度小于甲),sp.solve 返回空或负解,我们可以加入逻辑判断提示无法相遇,动画就不会标记错误时刻。

5. 小结

追及问题的动画化,真正耗时的往往不是画图,而是根据不断变化的题设反复列方程、解方程、算坐标。

SymPy 的价值在于把“根据题意列方程”和“求解”这两步都程序化了,你只需要用符号描述位置关系,剩下的事交给计算机。

这种思路同样适用于其他行程问题:相遇、环形跑道、顺流逆流……只要你能把物理情景转化为代数方程,

SymPy 就能帮你解出关键节点,而 Manim 负责把这些节点变成流畅的视觉呈现。

两个工具分工明确,做出来的动画既精准又灵活。

posted @ 2026-06-10 13:54  wang_yb  阅读(37)  评论(0)    收藏  举报