种群竞争模型详解:Lotka-Volterra方程与Python实战
2026/9/16 10:15:46 网站建设 项目流程

“种群竞争模型”在数学建模里出现的频率,远比你想象的高。国赛、校赛、美赛训练中,只要题目里出现“两个产品抢占同一市场”“两个物种争夺同一资源”“新旧技术争夺用户基数”这类设定,背后基本都能套进 Lotka-Volterra 竞争方程组。我当年第一次系统性学这个模型时,走了不少弯路,后来把推导、代码、参数分析和容易踩的坑全部整理成一整套笔记,这篇就是在笔记基础上重写的版本,非常适合准备数学建模竞赛、做生物数学课程设计,或者单纯想弄懂两个种群为什么有的能共存、有的会灭绝的读者来参考。

很多人把这个模型当作生态学专属内容,实际上它的应用范围广得多。企业竞争、舆论传播、疾病传播中的“双病原体竞争”都可以用同一套数学结构描述。理解它,不只是在学一个微分方程,而是在学一种“用资源约束和相互作用解释系统命运”的建模思维。这篇笔记会从模型设计、数学推导、Python 实操到问题排查,把全过程完整过一遍。

1. 整体设计与思路拆解

1.1 为什么选择 Lotka-Volterra 竞争方程组

数学建模中最怕的不是不会算,而是不知道用什么模型。种群竞争模型的核心逻辑可以用一个生活场景概括:两个公司卖同类产品,市场总容量有限,客户重叠度又高,一方多卖一份,另一方就少卖一份,这时候两家的销量随着时间怎么变化?如果把“公司销量”换成“种群数量”,把“客户重叠度”换成“生态位重叠度”,就得到竞争模型的标准设定。

两个种群在同一个封闭环境里,共享有限资源,各自单独存在时都遵循 Logistic 增长规律,也就是人口增长不能无限涨,最终会被环境容量限制住。当它们同时存在时,彼此会互相压制,压制的强弱用一对竞争系数 α₁₂ 和 α₂₁ 表示。这里的下标含义要记清楚:α₁₂ 表示一个种群 2 个体对种群 1 资源的消耗强度,相当于多少个种群 1 个体;α₂₁ 则反过来,表示一个种群 1 个体对种群 2 的压制程度。这个“相当于多少倍”的理解方式,是后面解读参数、向队友解释模型的钥匙。

我见过不少队伍在建模时,把竞争模型和捕食模型混在一起用。捕食模型的经典形式是 Lotka-Volterra 捕食方程,它的两个物种之间是一方受益、一方受害的关系,比如狼和羊;竞争模型则是两方都受害,比如两种羊抢同一片草。虽然都叫 Lotka-Volterra,但结构和结论完全不同。判断题目用哪个模型,只看一点:两个主体之间的相互作用是“你死我活”还是“互相拖后腿”。“互相拖后腿”就用竞争模型,这个区分在赛题分析里至关重要。

1.2 模型假设与适用场景

用这个模型之前,必须清楚它的假设边界。标准的竞争模型假设环境是均匀的、封闭的,没有迁入迁出;种群内的个体没有年龄结构,出生死亡是连续过程;种内竞争和种间竞争都是线性密度制约;环境容量和竞争系数不随时间变化。这些假设在真实生态里几乎不可能完全满足,但建模题考察的是“在合理假设下用数学语言描述问题”,只要把假设写清楚,模型就站得住脚。

可以用在以下几类典型场景:

  • 两种商品在同一个消费市场中的份额演化,市场容量对应环境容量,客户群重叠度对应竞争系数。
  • 新旧两代技术标准在推广期的用户争夺,新技术的“爆发力”对应内禀增长率,旧技术的基础用户规模对应初始值。
  • 两种疾病或两种谣言在人群中同时传播,医疗资源和公众注意力作为共享资源。
  • 同类创业公司抢占同一细分赛道,融资额和市场天花板作为环境容量。

在这些场景里,模型输出的是“最终稳态”,也就是谁胜出、谁消失、还是双方稳定共存。这个答案意义重大,因为它决定了企业该不该入场、政策该不该干预、技术路线该不该坚持。建模题里最常考的结论也就在这里。

2. 核心数学原理与参数细节

2.1 竞争方程逐项拆解

竞争模型的标准形式是一组常微分方程,用代码块展示如下:

dx1/dt = r1 * x1 * (1 - x1/N1 - α12 * x2/N1) dx2/dt = r2 * x2 * (1 - x2/N2 - α21 * x1/N2)

先看第一个方程。(r_1) 是种群 1 的内禀增长率,也就是在没有资源限制、完全没有种间竞争时,种群数量翻倍的速度。(x_1) 是当前种群规模,(N_1) 是环境容量。单独存在时,方程退化为 (dx_1/dt = r_1 x_1 (1 - x_1/N_1)),这是最经典的 Logistic 增长方程,当 (x_1) 远小于 (N_1) 时接近指数增长,当 (x_1) 接近 (N_1) 时增长率趋于零。

多出来的项 (- \alpha_{12} x_2/N_1) 很关键。它表示种群 2 的存在会额外占用种群 1 的资源。比如一家面包店每天能卖 1000 个面包,隔壁开了另一家面包店后,每有一个顾客去隔壁,这家店就少一个潜在订单。( \alpha_{12} ) 就是“隔壁一个顾客相当于自家多少个顾客”的换算比例。如果 ( \alpha_{12} = 0.8),那么每 10 个隔壁顾客相当于自家 8 个,压制不算太强;如果 ( \alpha_{12} = 2),那么隔壁每 1 个顾客相当于自家 2 个,竞争非常激烈。

这里有个容易忽略的点:( \alpha_{12} ) 和 ( \alpha_{21} ) 不一定相等,它们分别衡量两个方向上的压制强度,可以一个很大一个很小。现实中经常出现不对称竞争,比如入侵物种对本地物种压制极强,而本地物种对入侵物种几乎没影响。建模时如果题目没有明确说明,不要擅自假设两个系数相等。

2.2 平衡点与稳定性判别的核心推导

求平衡点,就是看系统最终可能稳定在哪些状态。令 ( dx_1/dt = 0) 且 ( dx_2/dt = 0),除了两个平凡的灭绝平衡点 ( (0,0) ) 之外,有三个值得关注的结果:

  • 种群 1 单独存活,种群 2 灭绝,写为 ( (N_1, 0) )。
  • 种群 2 单独存活,种群 1 灭绝,写为 ( (0, N_2) )。
  • 两个种群共存,内部平衡点记为 ( (x_1^, x_2^) )。

内部平衡点的求解来自两条零增长直线:

x1 + α12 * x2 = N1 α21 * x1 + x2 = N2

这两条直线在相平面上的交点就是共存状态。解这个二元一次方程组得到:

x1* = (N1 - α12 * N2) / (1 - α12 * α21) x2* = (N2 - α21 * N1) / (1 - α12 * α21)

单独看这个公式还不够,必须分析稳定条件。稳定性的标准做法是计算雅可比矩阵,把每个平衡点代入后看特征值实部的正负。我在这篇文章里不展开矩阵运算的全过程,直接给结论,因为实际建模中判断规则比记矩阵方便得多。

若内部平衡点存在且位于第一象限,并且满足 ( \alpha_{12} \alpha_{21} < 1 ),则它稳定,两个种群共存。判断条件是:

  • ( \alpha_{12} < N_1 / N_2 )
  • ( \alpha_{21} < N_2 / N_1 )

直观解释是:两个方向的种间压制都小于各自的“等效容量比”,互相伤害程度低于各自内部的密度制约,所以谁也干不掉谁,最终稳定共存。

若两个竞争系数都大于对应阈值,即 ( \alpha_{12} > N_1/N_2 ) 且 ( \alpha_{21} > N_2/N_1 ),那么两个边界平衡点 ( (N_1, 0) ) 和 ( (0, N_2) ) 都稳定,内部平衡点变成鞍点,系统进入双稳态。这时竞争结果取决于初始条件,谁一开始数量优势更大,谁就可能笑到最后。

若只有一方竞争系数大于阈值,比如 ( \alpha_{12} < N_1/N_2 ) 但 ( \alpha_{21} > N_2/N_1 ),那么稳定结局是种群 1 单独存活,种群 2 必然灭绝。这类“一边倒”的情况在赛题里最常考,结论简单干净,但推导过程要写清楚。

2.3 用四象限表格快速判断竞争结局

在实际比赛现场,没有时间每次都推一遍雅可比矩阵。我习惯把所有可能情况整理成一张判断表,看到参数直接查结论,非常高效。设 ( T_1 = N_1/N_2 ),( T_2 = N_2/N_1 ),四种情况对应关系如下:

条件内部平衡点稳定边界平衡点最终结局
( \alpha_{12} < T_1 ),( \alpha_{21} < T_2 )存在且稳定两个边界点都不稳定稳定共存
( \alpha_{12} < T_1 ),( \alpha_{21} > T_2 )不在第一象限仅 ( (N_1, 0) ) 稳定种群 1 胜出
( \alpha_{12} > T_1 ),( \alpha_{21} < T_2 )不在第一象限仅 ( (0, N_2) ) 稳定种群 2 胜出
( \alpha_{12} > T_1 ),( \alpha_{21} > T_2 )存在但为鞍点两个边界点都稳定取决于初始值

这张表我强烈建议抄在建模笔记首页。遇上赛题,先把参数算出来,再往表里一套,所有可能结局一目了然。判断完稳态再跑数值模拟验证,整个分析链条无懈可击。

3. 实操:Python 数值求解与可视化

3.1 可复用的代码框架

理论说完了,必须动手跑一遍。使用 Python 的 scipy.integrate.solve_ivp 做数值求解最稳妥。我写代码的习惯是先把核心函数独立出来,参数全部外部传入,方便后续改参数跑不同方案。

import numpy as np from scipy.integrate import solve_ivp import matplotlib.pyplot as plt def competition(t, z, r, N, alpha): x1, x2 = z dx1 = r[0] * x1 * (1 - x1 / N[0] - alpha[0] * x2 / N[0]) dx2 = r[1] * x2 * (1 - x2 / N[1] - alpha[1] * x1 / N[1]) return [dx1, dx2] def simulate(r, N, alpha, x0, t_max=80, num_points=1000): t_span = (0, t_max) t_eval = np.linspace(0, t_max, num_points) sol = solve_ivp(competition, t_span, x0, args=(r, N, alpha), t_eval=t_eval, method='RK45', rtol=1e-8, atol=1e-10) return sol.t, sol.y[0], sol.y[1] # 基础参数 r = np.array([0.5, 0.4]) # 内禀增长率 N = np.array([100.0, 100.0]) # 环境容量 alpha = np.array([0.6, 1.4]) # 竞争系数,对应种群2胜出 x0 = np.array([30.0, 30.0]) # 初始规模 t, x1, x2 = simulate(r, N, alpha, x0)

这里有几个参数要注意。rtolatol是相对误差和绝对误差的容差,默认值偶尔会在方程比较刚硬时产生负值或震荡,我习惯直接设到 (10^{-8})、(10^{-10}) 量级。method='RK45'适用于大多数非刚性问题,如果发现结果振荡异常,后面会讲怎么换成刚性求解器。

代码跑完后,画时间序列图:

plt.figure(figsize=(10, 4)) plt.plot(t, x1, label='Species 1') plt.plot(t, x2, label='Species 2') plt.xlabel('Time') plt.ylabel('Population') plt.legend() plt.grid(alpha=0.3) plt.show()

这张图输出什么,取决于竞争系数的设置,下面逐一测试。

3.2 四组参数实测对照

为了把 2.3 节的四种情况都验证一遍,我固定 ( N_1 = N_2 = 100 ),增长率 ( r_1 = 0.5 )、( r_2 = 0.4 ),初值两边都是 30。唯一改变的是竞争系数组合。

第一组,( \alpha_{12} = 0.6 ),( \alpha_{21} = 1.4 )。按判断表属于“种群 1 胜出”。跑出来种群 1 从 30 稳步涨到 100,种群 2 先是小幅涨到 35 左右,随后一路下降趋近于 0。模拟初期种群 2 的上涨很有迷惑性,看起来好像也能活,但稳态必然是灭绝。这就是为什么不能只看前 10 个时间单位就下结论,必须把时间拉长到完全收敛。

第二组,( \alpha_{12} = 1.4 ),( \alpha_{21} = 0.6 )。结果正好反过来,种群 2 胜出,种群 1 灭绝。这两组对比说明竞争系数决定了格局,初始优势在不对称竞争里帮不了弱的一方。

第三组,( \alpha_{12} = 0.6 ),( \alpha_{21} = 0.6 )。两个竞争系数都小于 1,互相抑制较弱。最终两个种群分别稳定在 62.5 和 62.5 左右,达到共存稳态。这里有个反直觉点:双方共存时的稳态规模都小于单独生活时的环境容量 100,为什么会这样?因为竞争虽然不足以让任何一方灭绝,但持续的资源挤压导致双方都无法达到单种群环境下的最大规模。这个结论在生态学和商业竞争里都有很强的现实意义。

第四组,( \alpha_{12} = 1.4 ),( \alpha_{21} = 1.4 )。双方互相压制都很强,内部平衡点为鞍点,两个边界点都稳定。我用初值 ( x_1 = 30, x_2 = 30 ) 跑,对称初值下系统陷入鞍点附近难以快速脱离,需要多试几组初值观察。改初值 ( x_1 = 70, x_2 = 30 ) 后,种群 1 胜出;改初值 ( x_1 = 30, x_2 = 70 ) 后,种群 2 胜出。这就是双稳态的直接证据。

把四组结果汇总成一张表,写论文时可以直接用:

竞争系数组合初值种群1最终状态种群2最终状态结论
0.6, 1.430, 30约100约0种群1胜出
1.4, 0.630, 30约0约100种群2胜出
0.6, 0.630, 30约62.5约62.5稳定共存
1.4, 1.430, 30双稳态临界双稳态临界结果依赖初值
1.4, 1.470, 30约100约0初值决定胜负
1.4, 1.430, 70约0约100初值决定胜负

3.3 相平面图:理解初值敏感性的最佳工具

时间序列图能看出最终结果,但看不出“为什么”。想理解不同初值为何走向不同结局,必须画相平面图。横轴是种群 1 规模,纵轴是种群 2 规模,图中画两条零增长线:一条是 ( x_1 + \alpha_{12} x_2 = N_1 ),一条是 ( \alpha_{21} x_1 + x_2 = N_2 )。这两条线把相平面分成四个区域,每个区域内两个种群的增长方向不同。

我通常用 quiver 画向量场,再叠加几条不同初值的轨迹,代码大致如下:

x1_grid, x2_grid = np.meshgrid(np.linspace(0, 120, 20), np.linspace(0, 120, 20)) u = r[0] * x1_grid * (1 - x1_grid / N[0] - alpha[0] * x2_grid / N[0]) v = r[1] * x2_grid * (1 - x2_grid / N[1] - alpha[1] * x1_grid / N[1]) plt.quiver(x1_grid, x2_grid, u, v, alpha=0.4) # 画零增长线 xs = np.linspace(0, 120, 200) plt.plot(xs, (N[0] - xs) / alpha[0], 'k--', label='dx1/dt=0') plt.plot(xs, N[1] - alpha[1] * xs, 'k-.', label='dx2/dt=0') # 画多条初值轨迹 for init in [(70, 30), (30, 70), (20, 20), (90, 90)]: t, x1, x2 = simulate(r, N, alpha, init) plt.plot(x1, x2, linewidth=1.5, label=f'init={init}') plt.xlabel('Species 1') plt.ylabel('Species 2') plt.legend() plt.show()

相平面图画出来之后,两条零增长线的交点一目了然。点落在哪条流线附近,就会往哪个平衡点走。对双稳态情形,两条零增长线在第一象限内交出一个鞍点,鞍点的稳定流形把相平面切成两个“流域”,初值落在哪一侧,终点就在哪一侧。这个直观认识能帮助你在赛题里快速解释“为什么这个条件对结果影响那么大”。

3.4 从实测数据反推竞争系数

多数数学建模赛题不会直接给你竞争系数,而是给一组时间序列观测数据,要求反推参数。这里我分享一个很实用的线性化方法,比赛现场手算都来得及。

把第一个方程两边同时除以 ( x_1 ),得到:

(dx1/dt) / x1 = r1 - (r1/N1) * x1 - (r1 * α12 / N1) * x2

左边是“单位种群规模的变化率”,右边是关于 ( x_1 ) 和 ( x_2 ) 的线性表达式。如果把离散观测数据近似计算 ( (x_1(t+\Delta t) - x_1(t)) / (x_1(t) \Delta t) ),再对 ( x_1 )、( x_2 ) 做二元线性回归,就能从回归系数中恢复出 ( r_1 )、( N_1 )、( \alpha_{12} )。第二个方程同理恢复 ( r_2 )、( N_2 )、( \alpha_{21} )。

这个方法的优势是快,不需要优化迭代;缺点是导数估计对数据噪声非常敏感。如果观测数据比较粗,导数的差分近似会产生巨大方差,反推出的参数可能完全不可信。稳妥的做法是:先用平滑样条对 ( x(t) ) 做拟合,再对平滑后的函数求导,最后做线性回归。竞赛中用这个流程能明显提升参数稳定度。

4. 常见问题与排查技巧实录

4.1 数值求解中的坑

用 solve_ivp 跑竞争模型时,最常见的异常是种群数量变成负数。方程里的 ( 1 - x_1/N_1 - \alpha_{12} x_2/N_1 ) 一旦跨过零,增长率为负,但 ( x_1 ) 理论上会趋近于零而不是穿过零。数值方法在接近零时如果步长太大,就可能越界得到负值,下一时刻解直接发散。解决办法有三个:一是把容差调小,二是换刚性求解器,三是在方程中强行把非负约束加进去。

第二个坑是时间尺度不合理。自习时我见过很多同学把时间范围设成 0 到 10,然后断言系统已经到稳态。实际上竞争模型的收敛时间取决于增长率大小,( r ) 是 0.01 量级时收敛时间极长,需要 1000 以上;( r ) 是 0.5 量级时几十个时间单位就稳定了。跑代码前先根据增长率量级粗略估算收敛时间,别拿同一个时间窗口套所有参数。

第三个坑是把 ( N_1 )、( N_2 ) 设得太悬殊。比如 ( N_1 = 100 ),( N_2 = 100000 ),那么两个方程的数量级差了好几个量级,普通 RK45 在刚度较大的情况下效率极低。此时建议改用method='LSODA'method='Radau',它们会自动切换策略,处理刚性系统更加稳定。实际操作中我用 LSODA 跑过很多组高量级差异的参数,结果都比 RK45 稳。

4.2 结果解读时的经典错误

有很多队伍模拟完只看谁“先涨得快”就下结论,这非常危险。种群初期增长速度受初值和环境容量影响很大,最终命运由竞争系数和内点稳定性决定。看结果必须先判断系统处于哪种稳态区间,再看初值落在哪个分支,否则很容易把一个暂态变化说成最终结局。

另一个错误是把“接近零”直接写成“灭绝”。数值模拟里种群数量降到 (10^{-6}) 不代表严格为零,应该写“数值上趋近于 0,理论稳态为灭绝”。如果论文里出现“种群数量下降到 0.0001 即认为灭绝”这种话,评委提问环节很容易被追问“这个阈值哪里来的”。规范写法是:分析边界平衡点的稳定性,说明该平衡点是局部渐近稳定的,数值结果显示在模拟时间范围内收敛到该平衡点。

还有一种常见的误读是,看到共存平衡点存在就认为一定会走向共存,忽略了内部平衡点可能是鞍点这一点。判断内点是稳定节点、稳定焦点还是鞍点,可以直接通过 ( \alpha_{12}\alpha_{21} ) 是否小于 1 来快速区分,也可以求雅可比矩阵特征值。在论文中列出这个判别过程,哪怕公式繁琐一点,也是加分项。

4.3 竞赛现场的经验总结

这条模型在建模比赛中非常好用,是因为它参数少、结果直观、可视化漂亮,而且有一套完整的稳定性理论支撑。相比机器学习模型跑来跑去给不出解释,竞争模型每一步推导都有明确的数学依据,评委问起来完全不怕。

比赛复盘时我总结了三条建议。第一,建模论文里不要只写“我们用 Lotka-Volterra 模型”,要把模型假设、参数含义、无量纲化过程写完整,特别是竞争系数的相对大小意味着什么,要用竞赛题目里的实际场景解释,比如市场份额、城市人口流动等。第二,数值实验要多做参数敏感性分析,把竞争系数在合理范围内上下浮动,看结论是否稳健。第三,一定要画相平面图,这是区分“会用模型”和“理解模型”的分水岭,也是评委最认可的可视化形式之一。

关于相平面图有一个包装技巧:把初值对应的轨迹用不同的颜色画出来,再用箭头标注时间方向,论文里再配一句“不同初值轨迹最终汇入同一平衡点,说明系统对初始扰动具有鲁棒性”,这句话的含金量远远高于单纯贴两张时间序列图。

这个模型后续还可以往三个方向扩展。一个是加入随机扰动,把确定性方程改成随机微分方程形式,研究环境波动对结局的影响。另一个是推广到三种群竞争,这时稳定性分析会复杂很多,但相平面变成了三维,结果更加丰富。还有一个是加入人为干预控制项,模拟投放资源扶持弱势种群、或者限制强势种群的情形,这在生态保护和商业策略赛题中都有很大的发挥空间。

我个人的体会是,学这个模型最大的收获不是会套公式、会调包,而是建立了一种“先判断稳态类型,再分析动态过程”的系统思维。每次拿到一个复杂问题,我都会下意识地问一句:这是一个竞争问题、捕食问题,还是一个合作问题?不同的相互作用结构对应完全不同的数学工具,想清楚这一点,建模就成功了一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询