简介一份面向数学建模竞赛与课程学习的幻灯片课件围绕动物群体的常微分方程模型系统讲解如何用微分方程刻画种群动态、分析平衡点稳定性并以ACM-85试题A为实例推导有限资源环境下最优捕捞策略与最大净利润条件。资源包仅含1个演示文稿文件大小约1.05MB内容高度凝练适合备赛学生、建模爱好者及相关课程师生快速查阅。目前已有113人学习属于小而精的建模方法资料。课件从单种群开发模型讲起覆盖收获率与最大可承受产量的关系、稳定与不稳定平衡点的判别并引入弱肉强食的Volterra模型展示狐兔数量交替波动的生态平衡过程同时结合价格、捕捞成本和增长率给出净利润最大化时捕捞方案的设计思路。通过这份幻灯片读者可以快速掌握将实际问题抽象为常微分方程、做定性分析并指导决策的完整链条为处理同类种群管理或资源优化题目提供可直接借鉴的建模步骤。1. 为什么数学建模题里的动物群体常微分方程模型值得单独练常微分方程建模在数学建模竞赛里的出现频率极高而动物群体是最容易入门的载体状态变量直接是种群数量方程结构直观结果又能落到“保护生态还是控制虫害”这类实际决策上。建模课件里讲动物群体的常微分方程模型时公式一般列得很清楚真正卡住大部分人的是后续那几步——参数改了不分析稳定性初值换了不观察数值解是否发散最后画出的图对不上题目要求。这篇文章按“先推方程再用 Python 求解器跑通最后用一个完整案例把参数影响、稳定性分析和调试方法串起来”的顺序展开。正在备赛的学生以及刚接触动态系统建模的开发者都可以顺着这条路径把一个动物群体建模题跑出有依据的相图与平衡点结论。2. 从单种群增长到捕食关联动物群体ODE模型的建立过程2.1 Malthus 模型为什么只能当初始版本对动物群体建模第一步是确定状态变量与时间尺度。对单一种群设第 t 时刻的种群数量为 N(t)把这个量对时间求导就得到方程。最原始的 Malthus 模型写作dN/dt r * N其中 r 称为内禀增长率量纲是“单位时间内单个个体对种群增长的贡献”。这个一阶线性方程的解是N(t) N(0) * exp(r*t)即指数增长。它适合描述资源充足的阶段性增长但真实生态系统中种群会受食物、生存空间等条件限制。引入环境承载力 K得到 Logistic 方程dN/dt r * N * (1 - N/K)非线性项(1 - N/K)体现了密度制约。当 N 远小于 K 时方程接近指数增长当 N 接近 K 时增长率趋零一旦 N 超过 K增长率变为负值种群回落。参数 r 决定趋近 K 的快慢K 决定长期平衡值这两个参数是后续参数估计的对象。这里不要匆匆带过。不少竞赛题的区分点就藏在追问里K 为什么是常数如果环境波动明显K 需要看成时间函数模型便从自治系统变为非自治系统后续相平面分析思路要跟着换。2.2 Lotka-Volterra 方程捕食、竞争、互惠共用一套符号规则两物种模型最经典的是 Lotka-Volterra 捕食-被捕食模型。设 x 为被捕食者数量y 为捕食者数量方程为dx/dt α*x - β*x*y dy/dt δ*x*y - γ*y参数含义α 是被捕食者在没有捕食者时的净增长率β 是捕食行为造成的猎物损失系数δ 描述猎物转化为捕食者生物量的效率γ 是捕食者在没有猎物时的死亡率。注意-β*x*y与δ*x*y来自同一次捕食事件但两个系数通常不等因为能量在营养级之间传递时有损耗。“状态变量 相互作用项”的写法可以覆盖其他物种关系整理成表关系状态变量含义方程形式最易出错的一项捕食x猎物y捕食者dx/dtαx-βxydy/dtδxy-γy±βxy竞争x、y 竞争同一资源dx/dtr1x(1-(xc1y)/K1)dy/dtr2y*(1-(yc2*x)/K2)交叉占用系数 c1、c2互惠x、y 互相促进dx/dtr1x(1-x/K1m1y)dy/dtr2y*(1-y/K2m2*x)m1*y 这一项写竞争或互惠模型时最容易被忽略的是环境承载力如何分配。两个物种共享同一种食物和生存空间那么 x 的承载力项应为1 - (x c1*y)/K1其中 c1 表示单位 y 个体对 x 资源的占用比例。c11 表示完全重叠c10 表示完全分离。先把这个写清楚再做参数标定可以避免后期模拟出现“两物种之间没有关系、曲线却一抬一落”的伪相关。2.3 建模时先过的两关量纲一致与非负约束写方程时我先做两个检查避免模型偏离实际。第一量纲一致性方程右端每一项都必须是“数量/时间”。β*x*y看起来是数量的平方但 β 本身带着1/(数量·时间)的量纲。写代码时不声明单位但参数标定必须保证同一套时间单位混用“天”和“月”是数值爆炸的最常见来源。第二状态变量非负真实种群数量不会为负ODE 解算器并不天然知道这个约束。参数或初值组合不合适时求解结果可能冒出负值此时要改参数区间而不是简单地把负值截断为零。截断动作会破坏解算器内部的连续性判断后面的轨迹会出现解释不通的拐角。3. 用SciPy把动物群体ODE跑通最小代码与求解器参数3.1 模型函数与求解器分离一套代码可复用SciPy 生态里积分接口推荐使用integrate.solve_ivp。早期常用的odeint在新的 SciPy 版本里已经处于遗留状态不放进新代码。我习惯把模型参数全部通过args传入求解器而不是把参数常量写在函数体内部这样后面的参数扫描可以直接复用同一个模型函数import numpy as np from scipy.integrate import solve_ivp import matplotlib.pyplot as plt def lotka_volterra(t, z, alpha, beta, delta, gamma): x, y z # z[0] 为猎物z[1] 为捕食者 dx alpha * x - beta * x * y dy delta * x * y - gamma * y return [dx, dy] params (1.1, 0.4, 0.1, 0.4) z0 [10, 3] t_span (0, 60) t_eval np.linspace(0, 60, 1200) sol solve_ivp( lotka_volterra, t_span, z0, argsparams, methodRK45, rtol1e-6, atol1e-9, t_evalt_eval, ) print(sol.success, sol.message)模型函数的参数顺序是时间 t、状态数组 z 以及四个模型参数。函数返回的dx、dy顺序必须与z[0]、z[1]对应这里先写猎物再写捕食者。solve_ivp的输入里t_span是积分区间t_eval只决定输出点的采样密度不影响内部自适应步长因此可以适当加密采样点让绘图更平滑。args的传入顺序与模型函数形参顺序一致后续用functools.partial固定一部分参数也很方便。3.2 返回值里先看这三个字段sol.success为 False 时说明积分提前终止sol.message会给出终止原因。sol.t是时间序列sol.y的形状为(状态数, 采样点数)。对两物种模型sol.y[0]是猎物曲线sol.y[1]是捕食者曲线。一个比较常见的错误是直接拿sol.y整体画图那样会得到两条形状相近的曲线容易看混。相平面图则要单独取sol.y[0]作为横坐标、sol.y[1]作为纵坐标这样轨道闭合性才看得清楚。3.3 求解器怎么选刚性问题先于“默认即可”求解器的选择不复杂按问题特性对号入座即可method适用场景常用容差设置RK45默认显式求解器适合大多数非刚性周期模型rtol1e-6atol1e-9LSODA刚性系统时间尺度跨多个数量级rtol1e-6atol1e-9Radau隐式求解器适合高度刚性或雅可比矩阵有大范围符号变化rtol1e-8atol1e-10判断是否刚性最省事的做法是保持同一组参数把 method 换成 LSODA 或 Radau比较计算量。如果 RK45 内部推进需要几千步而 LSODA 只用几百步说明原始问题是刚性的应当坚持用隐式方法。动物群体模型中的刚性通常来自增长率的时间尺度差异比如猎物以“天”为单位增长捕食者存活期以“月”为单位两边特征时间相差很大。容差参数的影响也值得单独说。rtol是相对误差容限atol是绝对误差容限。动物群体的状态变量在 0 附近徘徊时atol过大会容忍负值结果出现类似物种灭绝又莫名回到正值的情况。经验是把atol设在初始状态量级小 3 到 5 个数量级的位置初值在几十的量级时atol取 1e-9 到 1e-12 都比较合适。提示参数都写死在模型函数里不算错但会拖慢后续参数扫描。把参数扫描逻辑放在solve_ivp调用层用循环遍历参数网格灵敏度分析的代码结构会清晰很多。4. 兔与狐狸一个动物群体ODE建模案例的参数影响与输出检验4.1 参数来源与量纲换算竞赛题里参数通常需要从题目给的生态背景数据估算。以兔-狐系统为例假设兔子在没有狐狸时每周净增长率为 0.8即周率 α0.8每只狐狸每周对兔子群体造成的捕食压力系数 β0.4狐狸从每单位兔子生物量中获得增长的效率 δ0.1没有兔子时狐狸每周死亡率 γ0.4。代码里统一以“天”作为时间单位这几个参数都除以 7换算成日率后再传给solve_ivp。换算错误很隐蔽因为模型本身不会报警只会让输出曲线的时间尺度整体漂移。确认量纲是否统一的方法是把周率和日率两组参数各跑一遍比较平衡点位置平衡点应当落在同一位置只是到达平衡的快慢不同。若平衡点变了说明参数单位混用需要回到换算环节排查。4.2 基准模拟与相平面图继续使用第 3 章的模型函数完成模拟后绘制时间序列与相平面fig, axes plt.subplots(1, 2, figsize(10, 3.5)) axes[0].plot(sol.t, sol.y[0], labelrabbit) axes[0].plot(sol.t, sol.y[1], labelfox) axes[0].set_xlabel(time (days)) axes[0].set_ylabel(population) axes[0].legend() axes[1].plot(sol.y[0], sol.y[1]) axes[1].axhline(alpha / beta, colorgray, linestyle--) axes[1].axvline(gamma / delta, colorgray, linestyle--) axes[1].set_xlabel(rabbit) axes[1].set_ylabel(fox) plt.tight_layout()相平面里的竖直虚线是猎物零增长线x* gamma/delta 4水平虚线是捕食者零增长线y* alpha/beta 2.75交点就是平衡点。注意两条线的方向猎物没有净变化时得到的是捕食者数量的固定值对应水平方向的 y 值捕食者没有净变化时得到的是猎物数量的固定值对应竖直方向的 x 值。把这两条线画反之后后续所有稳定性讨论都会偏离。4.3 单参数扰动周期比稳态更值得记录保持其他参数不变只把 β 从 0.4 提高到 0.6捕食效率上升后时间序列的振荡周期会变短振幅也会增大。再把 γ 从 0.4 降到 0.2捕食者死亡率降低平衡点向右移动。单参数扫描需要记录两个量平衡点位置与振荡主周期。主周期可用scipy.signal.find_peaks对sol.y[0]做峰值检测后取相邻峰间隔的平均值from scipy.signal import find_peaks peaks, _ find_peaks(sol.y[0]) # 只对猎物时间序列检测峰值 periods np.diff(sol.t[peaks]) print(mean period:, periods.mean(), days)如果数值噪声干扰了峰值检测可以先对sol.y[0]做一次滑动平均再交给find_peaks。这段代码本身简单但说明一个建模习惯ODE 模型的可观察量不只包含稳态均值振荡周期和相位关系同样有分析价值。竞赛报告里写出“系统周期随捕食效率上升而变短”这类结论比只贴一张图更有信息含量。4.4 初值改变与参数改变要分开讨论初值对 ODE 演化的影响需要分模型区别对待。对单一物种 Logistic 模型初值只影响趋近 K 的路径不影响最终平衡值对 Lotka-Volterra 捕食模型不同初值落在相平面不同的闭合轨道上参数决定平衡点的位置初值决定环绕中心走哪条轨道。报告中每写一个动力学结论都要先确认它是由参数驱动还是初值驱动。切换初值后如果系统形态发生质变比如从闭合轨道变成发散的螺旋说明模型里可能还有另一个不稳定平衡点需要进入下一章的稳定性分析去确认。5. 稳定性分析与数值排错动物群体ODE模型发散排查5.1 平衡点与雅可比矩阵从零增长线到特征值判断相平面里零增长线的交点就是平衡点但平衡点是否稳定要看平衡点处雅可比矩阵的特征值。对 Lotka-Volterra 模型做扰动展开雅可比矩阵为J [[α - β*y, -β*x], [δ*y, δ*x - γ]]在平衡点(x*, y*) (γ/δ, α/β)处矩阵变成J [[0, -β*γ/δ], [δ*α/β, 0]]特征值是纯虚数系统在平衡点附近做周期振荡不收敛也不发散。用代码验证import numpy as np alpha, beta, delta, gamma 1.1, 0.4, 0.1, 0.4 x_star gamma / delta y_star alpha / beta J np.array([ [0, -beta * gamma / delta], [delta * alpha / beta, 0], ]) eigvals np.linalg.eigvals(J) print(特征值:, eigvals)输出是一对共轭纯虚根。纯虚根对应的运动形态是环绕平衡点的闭合轨道而不是向内收敛的螺旋。这个差别在相平面图上肉眼可见闭合轨道意味着系统对初值有记忆同一组参数下不同初值对应不同振幅的振荡。现实中纯虚根很难长期成立环境随机波动会把轨道推离原闭合曲线因此实际建模常把猎物方程加上密度制约项改成dx/dt α*x*(1 - x/Kx) - β*x*y运动特征随之从中性稳定变为阻尼振荡。5.2 数值发散时的三步排查“模拟曲线飞到天上去”的原因多数不是模型写错而是求解器设置与模型时间尺度不匹配。按顺序排查效率最高。第一步显式设置max_step。solve_ivp默认最大步长可能偏大当参数数量级很小时这一步会跨过高频振荡周期轨迹直接跳到系统范围之外画出来就是高速跳变线。设max_step0.1后观察结果是否稳定。第二步切换求解器。同一组参数同时用 RK45 和 LSODA 跑一遍比较sol.y的最大值。如果 RK45 的结果到了 1e6LSODA 的结果在几百这个量级基本可判定是刚性问题引起的数值不稳定不是方程本身发散。第三步查时间单位。周率与日率的混用是低级却高发的错误。把整组参数都除以 7 后重跑如果平衡点位置不变、只有趋近速度变化说明换算正确如果平衡点位置漂移就从每个参数进入方程的系数开始查不要只盯模型函数里的那几行。5.3 用断言提前暴露问题不靠肉眼扫图参数扫描过程中建议把单次求解封装起来在内层加断言def solve_lv(params, z0, t_span): sol solve_ivp(lotka_volterra, t_span, z0, argsparams, methodRK45) assert sol.success, fsolver failed at params {params}: {sol.message} return sol只要某一组参数让积分器失败异常信息会直接指出是哪一组参数引起的。sol.status字段同样有用0 表示正常1 表示事件触发提前停止-1 表示积分失败。没有自定义事件时 status 几乎总是 0一旦出现非 0 状态优先读sol.message的文本而不是反复调整画图范围去猜测原因。6. 用零增长线图快速完成动物群体ODE的参数区间设计6.1 零增长线是建模学习阶段的调试器零增长线图在第 4 章已经画过它的价值在于比时间序列更容易看出参数变化的方向。把猎物方程置零得到一条关于 y 的直线把捕食者方程置零得到一条关于 x 的直线。参数改变时这两条线在相平面里平移交点的移动就是平衡位置的变化。调参之前先用几何工具把可行域框出来能省下大量无效的积分运算。6.2 用代码批量过滤无意义参数组合参数区间设计不需要只靠手工点图。写一组循环遍历 α、β、δ、γ 的候选区间把平衡点不落在第一象限的组合过滤掉gamma, delta 0.4, 0.1 alpha_range np.linspace(0.5, 2.0, 50) beta_range np.linspace(0.2, 0.8, 50) usable [] for alpha in alpha_range: for beta in beta_range: x_star gamma / delta y_star alpha / beta if x_star 0 and y_star 0: usable.append((alpha, beta))这里y_star alpha / beta的正性要求其实由参数本身为正自动满足但这套写法可以扩展出更严格的条件比如要求x_star与y_star都落在某个生态观测区间内。把零增长线的代数条件写进筛选逻辑后整个参数网格扫描就变成了预报步骤而不是事后整理结果。6.3 三步闭合成一套固定流程常见的做法是把流程固定成三步先用零增长线筛选平衡点位置给出参数可行区间再用solve_ivp对区间内有代表性的点计算时间序列最后回到雅可比矩阵判断稳定类型。每一步都能验证前一步的结果写报告时只需保留最后一步的图。绘图调试有一个小技巧检查零增长线位置时用灰白底的单色图不要加颜色填充颜色会干扰平衡点附近微小偏移的判断。本文还有配套的精品资源点击获取