简介:2003年全国大学生数学建模竞赛D题(抢渡长江)的参考论文,面向竞赛参赛者、指导教师及数学建模爱好者,完整解析了这道经典最优控制问题。文档以恒定游泳速度和受水流影响的渡江场景为背景,从运动方程推导出连接起终点的直线路径,引入路径斜率z与到达时间T的关系,并通过二次方程与可行性条件筛选最优解;随后给出具体参数下的角度、速度与时间计算示例,还讨论了垂直游动无法到达终点的情况。针对水流速度分成三段变化的复杂情况,进一步给出分段调整游泳角度的优化方案,并附有枚举法计算结果,体现模型从简单到复杂的完整演进。压缩包内共1个doc文档,大小124KB,内容精炼、公式完整,可直接作为竞赛备赛或课程案例参考。已有776人学习下载,适合需要系统理解赛题建模思路、掌握最优控制求解方法和借鉴优秀论文写作结构的读者。
1. 数学建模2003年全国赛D题参考论文拆解:最优控制、直线与二次方程
如果你第一次翻开数学建模2003年全国赛D题参考论文,第一反应大概率是:这不是一道物理题吗?一个游泳者要从起点游到终点,水流速度已知,自己的游速恒定,问怎么游最快。但它真正考的是最优控制问题——控制量是游泳方向角θ(t),目标是最小化到达时间T。最反直觉的结论是:在流速均匀的流场里,中途拐弯不仅不会更快,反而一定不如全程保持同一个方向角。这道题把“找最优路径”的变分问题,最终压缩成了一个一元二次方程。当年这道题被称为“抢渡某江”,题目给的参数很具体:江宽1160米,顺流方向位移1000米,流速1.89米/秒。适合三类人读:正在备赛的学生、带建模竞赛的指导教师,以及想看看最优控制如何降维成代数方程的一线工程师。
2. 直线路径假设与二次方程:把渡江最优控制压成一元求解
2.1 运动方程与“直线最优”的直觉
模型的第一步是把游泳者当成质点。设顺流方向为x轴,垂直江岸方向为y轴,游泳者相对水的速度大小为u,方向角θ为游速与x轴正向的夹角。水流速度为v,方向沿x轴正向。那么运动方程很简单:
$$\frac{dx}{dt} = u\cos\theta + v, \quad \frac{dy}{dt} = u\sin\theta$$
这里的θ(t)是控制量。游泳者想从(0,0)到达(L,H),总时间T是性能指标。这是一个标准的最优控制问题:状态方程线性,控制量θ只通过三角函数进入右侧,边界条件固定。
参考论文里引了一个最优控制教材中的结论:在连续控制函数条件下,若流场是均匀常数流,则θ(t)等于常数时问题有最优解。翻译成人话:没必要中途改变游泳方向。物理直觉也很通顺——水速和游速不随时间、不随位置变化,你在前半程和后半程面临的环境完全一样,没有任何理由让前半程和后半程的策略不同。中途拐弯只会让某个方向的分量变小,而不会换来任何额外收益。
这个结论的价值在于降维。原本要解泛函极值,现在只需要猜一个固定的角度,然后代数求解。后面的所有公式都建立在这个“直线路径”假设上。如果流场分多段且各段水速不同,则每一段内部仍然走直线,只是段与段之间的角度会不同,整体是一条折线。
2.2 消去时间的代数过程:从两个积分式到二次方程
固定角度后,运动方程可以直接积分。到达终点时满足:
$$L = (u\cos\theta + v)T, \quad H = u\sin\theta \cdot T$$
两个方程两个未知数(θ和T)。我习惯先定义两个中间量,把三角函数关系变成代数关系。令p = u sinθ,它表示游泳者自己贡献的垂直江岸方向速度,也等于H/T。再令q = u cosθ = L/T − v,它是游泳者自己贡献的顺流方向速度。
因为q² + p² = u²,把上面的关系代入:
$$\left(\frac{L}{T} - v\right)^2 + \left(\frac{H}{T}\right)^2 = u^2$$
将T = H/p代入并整理,就得到一个关于p的一元二次方程:
$$(H^2 + L^2)p^2 - 2LvH \cdot p + H^2(v^2 - u^2) = 0$$
判别式为:
$$\Delta = 4H^2\left[(H^2 + L^2)u^2 - H^2 v^2\right]$$
这个判别式非常关键。它给出可达性条件:只有当Δ ≥ 0,也就是u必须大于某个下限值时,游泳者才有可能到终点。后面第3章会专门算这个下限。方程的两个根中,p大对应T小,因为T = H/p,而我们要的是最短时间,所以取较大的根。
2.3 参数化求解函数:一个可复用的Python实现
把上面的推导写成函数,后面所有分段计算都复用这一段。代码里直接返回可达性门槛、最优纵向速度分量、最短时间和方向角。
import math def solve_constant(L, H, v, u): # 参数单位统一为 m, s, m/s u_min = v * H / math.hypot(L, H) # 可达最小游速 if u < u_min: return None, None, None, u_min A = H * H + L * L B = -2 * L * v * H C = H * H * (v * v - u * u) delta = B * B - 4 * A * C if delta < 0: return None, None, None, u_min p = (-B + math.sqrt(delta)) / (2 * A) # 取较大根,T 更小 T = H / p # 由 L/T = v + u*cos(theta) 反推方向角 cos_theta = (L / T - v) / u theta_deg = math.degrees(math.acos(cos_theta)) return p, T, theta_deg, u_min # 用题目给的2002年赛况参数验证 p, T, theta, u_min = solve_constant(1000, 1160, 1.89, 1.5) print(f"p={p:.4f}, T={T:.1f}s, theta={theta:.2f}°, u_min={u_min:.3f} m/s")这段代码的逻辑很直白:先用u_min做前置判断,不满足就直接返回None,避免后续开方得到NaN;二次方程用求根公式,取“+”号对应的大根。反推方向角时用acos,因为(L/T − v)/u 可能是负值,acos会自动映射到90°到180°的范围——这正是需要“逆着水流斜游”的情况。
运行结果:T ≈ 910.4秒,即15分10秒,方向角约121.9°。这和参考论文里给出的一致。如果把u换成1.54,T会压到848秒附近,正好对应当年第一名的成绩。
3. 用赛况数据校核模型:反推848秒成绩与1.43m/s的速度门槛
3.1 从成绩反推游泳参数
题目给了两组可以互相校核的数据:第一组成绩848秒,第二组设u=1.5米/秒算出910秒。反过来做更有工程意义——已知终点坐标、水速和到达时间,反推游泳者实际的平均游速和方向角。这在赛后数据分析里很常见。
L, H, v, T = 1000.0, 1160.0, 1.89, 848.0 p = H / T # 垂直江岸方向的速度分量 vx = L / T - v # 游泳者自身贡献的顺流速度分量 u = math.hypot(vx, p) # 实际游速 theta = math.degrees(math.atan2(p, vx)) print(f"u={u:.3f} m/s, theta={theta:.2f}°")这里有两个容易看走眼的点。第一,L/T是净顺流速度,减去水速v之后得到的是游泳者自己贡献的顺流分量,这个值算出来约-0.71米/秒,是负的。也就是说,冠军实际上在顶着水流往上游方向游,靠水流把自己冲到终点。第二,angle用atan2(p, vx)而不是atan(p/vx),因为vx是负数,直接相除会得到错误象限。
输出结果:u ≈ 1.54米/秒,θ ≈ 117.5°。这个游速放在现实里是相当高的水平,但还在人类短距离爆发范围内。参考论文用这个结果说明:模型算出的最优路径参数和真实比赛成绩能对上,模型本身是可信的。
3.2 垂直硬游为什么不可能
题目里专门讨论了一个极端情况:游泳者始终垂直江岸方向游,也就是θ恒等于90°。这时顺流方向完全靠水流推,到达对岸的时间由L = vT决定,T = 1000 / 1.89 ≈ 529秒。要在529秒内横渡1160米,垂直方向速度需要u = H/T ≈ 2.19米/秒。
2.19米/秒是什么概念?男子1500米自由泳的世界纪录平均速度大概在1.7米/秒左右。普通业余选手长距离巡航速度能到1.2米/秒已经很不错。也就是说,垂直硬游需要运动员以远超长距离世界纪录的速度横渡一条江,这在现实中不可能。参考论文的原话是“永远游不到终点,被冲到终点的下游去了”——准确说法应该是:在当时参赛者的实际速度水平下,垂直硬游的人会被冲向下游,无法在目标点登岸。
这个例子的建模价值不在于计算本身,而在于说明一个道理:最优角度不是90°,而是大于90°的钝角。游泳者必须有意识地向上游方向斜游,用自身游速的逆流分量去抵消一部分水流,才能让净位移指向终点。
3.3 可达性门槛:为什么u>1.43m/s是硬条件
第2章判别式Δ ≥ 0可以化简成一个非常干净的不等式。由Δ = 4H²[(H²+L²)u² − H²v²] ≥ 0,得到:
$$u \geq \frac{vH}{\sqrt{H^2 + L^2}}$$
这就是游泳者能够到达终点的最小游速。注意分母是起点到终点的直线距离,所以这个门槛可以理解成“水速在直线距离方向上的投影”。
把2002年赛况的H=1160、L=1000、v=1.89代入:
L, H, v = 1000.0, 1160.0, 1.89 u_min = v * H / math.hypot(L, H) print(f"u_min = {u_min:.3f} m/s")得到约1.431米/秒。也就是说,如果你的游速低于1.43米/秒,无论怎么选角度都到不了终点。这解释了为什么题目里设置u=1.5米/秒——刚好压在门槛之上,能到但时间不宽裕。
更有意思的是1934年那组历史数据:比赛全程直线距离约5000米,垂直距离仍为1160米,那么顺流方向距离L = √(5000² − 1160²) ≈ 4864米,v仍取1.89。代入门槛公式得到u_min ≈ 0.438米/秒,连0.44都不到。因为顺流距离很长,水流有充足时间把游泳者送到下游,选手只需要很慢的游速就能完成横渡。这组对比强烈说明:不同年份的赛道形状不同,“能否完赛”的速度门槛差异巨大,不能拿同一把尺子去衡量。
4. 三段变流速模型:枚举法复现904秒,连续流速段精修到892秒
4.1 三段模型的分段描述与可达条件
真实江水的流速不是均匀的,靠岸区域慢、江心区域快。参考论文把江宽分成三段:H1 = H3 = 200米,H2 = 760米;对应水速v1 = v3 = 1.47米/秒,v2 = 2.11米/秒;游泳者速度仍为u = 1.5米/秒。这样的分段取值不是随便拍的,它保证三段按深度加权后的平均流速仍约等于1.89米/秒,与报载数据一致。
注意第2段水速2.11米/秒大于游速1.5米/秒。在这种“快水区”,游泳者必须朝上游方向游出更大的角度,否则净顺流速度会太大,根本到不了预定的下游位置。这里有一个易被忽略的约束:对于水速大于游速的第i段,横向距离Li不能太小。由可达性判据反解:
$$L_i > \frac{H_i \sqrt{v_i^2 - u^2}}{u}$$
代入第2段数据:H2=760,v2=2.11,u=1.5,得到L2 ≈ 751.6米。所以题目里枚举L2从760米起步是有道理的——小于这个值,第2段方程无实根。这个条件在编程时尤其重要,少写这个判断,枚举到不可行区间会直接得到NaN。
第1段和第3段是慢水区,v1=v3=1.47 < u,对L1没有下限要求,只需要L1≥0即可。
4.2 枚举法复现:粗网格可以直接跑出904.58秒
分段模型下,总路径由三段直线组成,整体呈折线。由于两端的慢水区对称,L1 = L3 = (1000 − L2) / 2,所以只需要对L2做一维枚举。每段都用第2章的solve_constant函数计算时间和角度。
import math def seg_time(Lseg, Hseg, vseg, u=1.5): if u * u * (Hseg * Hseg + Lseg * Lseg) <= vseg * vseg * Hseg * Hseg: return None, None A = Hseg * Hseg + Lseg * Lseg B = -2 * Lseg * vseg * Hseg C = Hseg * Hseg * (vseg * vseg - u * u) delta = B * B - 4 * A * C p = (-B + math.sqrt(delta)) / (2 * A) Tseg = Hseg / p vx = Lseg / Tseg - vseg theta = math.degrees(math.acos(vx / u)) return Tseg, theta best = None for L2 in range(760, 1001, 20): L_rem = 1000 - L2 for L1 in range(0, L_rem // 2 + 1, 10): L3 = L_rem - L1 T1, th1 = seg_time(L1, 200, 1.47) T3, th3 = seg_time(L3, 200, 1.47) T2, th2 = seg_time(L2, 760, 2.11) if None in (T1, T2, T3): continue total = T1 + T2 + T3 if best is None or total < best[0]: best = (total, L1, L2, th1, th2, T1, T2) total, L1, L2, th1, th2, T1, T2 = best print(f"L1=L3={L1}m, L2={L2}m, T={total:.2f}s, " f"theta1={th1:.2f}°, theta2={th2:.2f}°")这段代码用两层循环:外层枚举L2,内层枚举L1。seg_time函数先做可行性判断再算根,避免水速大于游速时强行开方。运行结果:L1=L3=100米,L2=800米,T≈904.58秒,约15分05秒;对应θ1=θ3≈124.66°,θ2≈119.19°。这和参考论文给出的枚举表完全一致。
为什么枚举步长是20米不是10米?参考论文的表格里外层L2从760到1000每20米一段,内层L1从120到0每10米一段。这个粒度下最优是L2=800。如果改用更细的步长,最优会出现在L2≈806米附近,总时间能再压0.5秒多。这就是枚举法的固有权衡:网格越细越接近真最优,但计算量线性增长。对这道题来说,单次求解是常数时间,完全可以把步长改到5米甚至1米再验一次。
4.3 连续流速段的处理:等效平均速度与892.56秒
题目第4问把第1段从“分段常数流速”改成“流速连续变化”。参考论文里这段用了抛物线轨道的推导,原公式排版比较乱,这里给一个能复现其数值结果的等效做法:把连续变化的流速剖面按空间平均,等效成一个常值速度。若流速从岸边0线性增加到最大值2.28米/秒,则空间平均速度为峰值的一半,即v̄ = 1.14米/秒。第3段同理。第2段流速仍取2.28米/秒。
best = None for L2 in range(880, 1001, 20): L_rem = 1000 - L2 for L1 in range(0, L_rem // 2 + 1, 10): L3 = L_rem - L1 T1, th1 = seg_time(L1, 200, 1.14) # 连续段等效平均流速 T3, th3 = seg_time(L3, 200, 1.14) T2, th2 = seg_time(L2, 760, 2.28) # 快水区,注意可达条件 if None in (T1, T2, T3): continue total = T1 + T2 + T3 if best is None or total < best[0]: best = (total, L1, L2, th1, th2, T1, T2) print(f"等效模型最优: L1=L3={L1}m, L2={L2}m, T={total:.2f}s")注意两个关键变化。第一,第2段流速变成2.28米/秒后,套用可达性公式可以算出L2必须大于约870米,所以枚举从880米开始,而不是760米。第二,第1段用1.14而不是2.28,因为连续变化段的平均水速贡献远小于最大值。如果用2.28直接套常数公式,第1段会直接判定不可达,得到None。
运行结果:L1=L3=40米,L2=920米,T≈892.56秒,约14分53秒;θ1=θ3≈126.87°,θ2≈115.04°。这对应参考论文里连续流速模型的最优解。如果换用解析求导代替枚举,可以得到L2≈922.9米,T≈892.5秒,两者差不到0.1秒,说明这个网格分辨率下枚举结果已经很可靠。
5. 避坑与常见问题排查:五个让模型结果翻车的地方
现象一:算出300多秒的“超快成绩”
用二次方程求解时,有人得到T≈373秒,比冠军成绩还快一倍多,自己都觉得不对。原因是p取了小根,或者方向角算了锐角。方程两个根对应两条可达路径:一条是大幅逆流斜游,耗时较长;另一条是顺流斜游,净横向速度太大,虽然数学上满足运动方程,但对应的方向角在现实中意味着游泳者几乎被水流冲走,不满足“控制方向”的物理直觉。解决方法是明确取较大p根,然后用acos反推角度并确认θ落在90°到180°区间。我的自查习惯是打印一下L/T,也就是净顺流速度,若它大于u+v,说明符号错了。
现象二:程序报错或输出NaN
把1934年那组数据直接代入时,方程的判别式小于0,代码里如果没做判断,sqrt会返回nan。原因很简单:忘了先算可达性门槛。任何分段模型里,只要某段水速大于游速,这一段的横向距离就有下限。解决方法是把u_min = vH/√(H²+L²)作为seg_time函数的第一道门槛,小于它直接返回None,主循环里跳过。这样即使枚举区间覆盖不可行区域,程序也不会崩。
现象三:粗网格错过了真正最优的L2
参考论文里枚举表给出的最优是L2=800米,T=904.58秒;但解析法得到的最优是L2=806.33米,T=904.02秒。差距只有0.5秒,但如果你在论文或答辩里说“最优L2=800米”,严谨的评阅人会挑出来。这不是模型错,而是枚举步长带来的离散误差。解法是两层策略:先粗枚举确定大致区间,再在区间内用5米甚至1米步长精扫,或者直接对总时间函数做数值求导。我的习惯是粗扫一遍后用scipy.optimize.minimize_scalar收尾,几行代码就能把步长影响消掉。
现象四:连续流速段硬套常数公式
把第4问第1段的最大流速2.28直接当成常数水速代入,seg_time返回None,或者即使能算,角度和时间也完全对不上。原因在于“连续变化”和“分段常数”是两种不同流场:线性分布的流速剖面,游泳者轨迹是抛物线,其等效平均水速是峰值的一半,而不是峰值。解决方法是先看题目的流速剖面描述,再决定用常数模型还是抛物线模型。对这道题,把连续段等效为v̄ = 峰值/2,就能精确复现参考论文的枚举数值。如果流速剖面不是线性的,比如是二次曲线,那就老老实实对运动方程做数值积分,别偷懒。
现象五:单位混用,结果量级完全不对
有人把L=1000米写成1千米,或者水速1.89米/秒写成1.89千米/小时,算出来的u_min只有0.4米/秒,然后惊讶为什么1934年大家都能轻松完赛。这道题所有公式都是按米和秒推导的,只要有一个量换算错了,二次方程的系数就全乱。解决方法是把入口参数全部统一成SI单位,并在函数开头加一个合理范围断言:L和H在几百到几千米量级,v和u在0到5米/秒量级。超过就直接抛异常。这个习惯能帮你省掉至少半小时的排错时间。
6. 用欧拉仿真给解析解上保险:终点坐标自检法
解析解算完,很多人直接拿T去交差。但我建议多走一步:把解析得到的分段角度和时间塞进一个最简单的欧拉积分器,模拟游泳者从(0,0)出发的完整轨迹,最后看终点坐标是不是(1000,1160)。这一步能暴露所有象限错误、符号错误和段间拼接问题。
def simulate(segments, u=1.5, dt=0.01): x = y = t = 0.0 for _, _, v, theta_deg, Tseg in segments: rad = math.radians(theta_deg) steps = int(Tseg / dt) for _ in range(steps): x += (u * math.cos(rad) + v) * dt y += u * math.sin(rad) * dt t += dt return x, y, t # 用第4问等效模型的最优分段结果做自检 segs = [ (40, 200, 1.14, 126.87, 166.68), (920, 760, 2.28, 115.07, 559.40), (40, 200, 1.14, 126.87, 166.68), ] x_end, y_end, t_total = simulate(segs) print(f"终点: ({x_end:.2f}, {y_end:.2f}), 总时间: {t_total:.2f}s")输出终点约在(999.9, 1159.9)附近,总时间约892.8秒。和解析解的终点误差在0.1米内,说明分段角度和时间是自洽的。如果终点偏差超过1米,优先怀疑某个θ的象限错了;如果偏差沿x方向系统性偏移,多半是某段v的符号或数值用错。
这个自检法也是我自己的血泪教训。有一次分析某跨平台运动仿真项目时,优化出的分段轨迹解析值看起来完美,但一上欧拉仿真,终点漂到了下游40米。查了半天发现是第2段角度只算了锐角,忘记了钝角分支。从那以后,我每次做这类分段轨迹优化,都强制把解析解过一遍数值仿真的终点自检,谁跟我合作都要走这一步。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取