简介:这份资料围绕高中物理「小船渡河问题」展开,面向备战高考的学生及物理教师,系统梳理运动的合成与分解在该专题中的应用。内容涵盖垂直渡河、最短时间渡河、最小位移渡河与最小速度渡河四类典型情景,并归纳同时性、独立性、等效性、同一性等运动特点,以及等效替代思想、平行四边形法则、正交分解与矢量三角形等方法,帮助读者把复杂问题简单化。资源包内含1个doc文档,压缩包约1.21MB,以说课稿形式呈现教材地位、教学目标、重难点、教法与教学链条,并配有例题与多道期中真题练习。目前已有153人学习,适合需要理清渡河模型、掌握解题思路并查漏补缺的读者参考。
1. 小船渡河问题到底在算什么:从一道物理题到可复现的数值仿真
很多人第一次见到“小船的渡河问题”是在中学物理卷子上:一条船在流水里横渡,船头朝向和河岸有个夹角,问最短时间怎么走、最短位移怎么走。纸面上两个公式一联立就出答案,可一旦把场景搬到工程里——比如无人船横渡一段河道、水下航行器穿越洋流区、甚至游戏里 NPC 寻路——题目立刻变成“给定流速场和船速上限,求一条既快又不被冲偏的轨迹”。这时候解析解基本失效,必须靠数值方法。
这篇笔记就围绕“小船的渡河问题”这个经典模型,把它从纸面公式一路推到可运行的仿真代码。核心要解决三件事:船头朝向和实际航迹为什么不是一回事、流速不均匀时最短时间怎么求、以及怎么用几十行 Python 把轨迹画出来并验证。适合正在做路径规划、无人载具控制、或者单纯想把物理题吃透的从业者。下面所有推导和代码都可以直接抄去改参数复现。
2. 先分清船头朝向、航迹方向和流速矢量:三个速度别混
2.1 速度三角形是全部推导的起点
小船渡河问题的所有玄学,根源都在于“船在静水中的速度”和“船相对岸的速度”是两个不同的量。设船在静水中的速度矢量为 $\vec{v}_b$,水流速度矢量为 $\vec{v}_c$,那么船相对岸的实际速度是:
$$\vec{v} = \vec{v}_b + \vec{v}_c$$
注意 $\vec{v}_b$ 的方向由船头决定,大小固定为船速上限 $v$;$\vec{v}_c$ 由水流决定,通常沿河岸方向。船实际走出来的航迹方向是 $\vec{v}$ 的方向,而不是船头方向。这就是为什么船头斜着指向上游,船却能垂直横渡——船头方向抵消了水流分量。
把矢量拆到坐标系里更清楚。设河岸方向为 x 轴,垂直河岸指向对岸为 y 轴,河宽为 $d$。水流 $\vec{v}_c = (u, 0)$,船头与 y 轴夹角为 $\theta$(偏向上游为正),则:
$$\vec{v}_b = (-v\sin\theta,\ v\cos\theta)$$
合速度:
$$\vec{v} = (u - v\sin\theta,\ v\cos\theta)$$
横渡时间只由 y 方向分量决定:$t = d / (v\cos\theta)$。要让 $t$ 最小,就要让 $\cos\theta$ 最大,即 $\theta = 0$,船头正对对岸。此时 $t_{min} = d/v$,但船会被水流冲向下游,横向位移 $x = u \cdot t_{min} = ud/v$。
反过来,如果要航迹垂直河岸(位移最短,等于河宽 $d$),需要 x 方向合速度为零:$u - v\sin\theta = 0$,即 $\sin\theta = u/v$。这要求 $u < v$,否则船根本压不住水流,无论怎么指都会被冲下去。这个 $u < v$ 的条件就是很多题目的隐含前提,工程里如果流速超过船速,最短位移方案直接不存在,只能退而求其次找“被冲得最少”的朝向。
2.2 最短时间和最短位移为什么不能同时满足
这两个目标本质上是矛盾的。最短时间要求把所有船速都用在横渡上,一点不留给抵消水流;最短位移要求分出一部分船速去抵消水流,横渡分量必然变小,时间变长。用一句话概括:省时间就要接受被冲偏,省位移就要接受慢。
工程上怎么选?看任务约束。如果下游有障碍物或者必须精确到达某个码头,位移优先;如果只是尽快到对岸、下游空间充足,时间优先。还有一种折中:给定一个允许的最大横向偏移 $x_{max}$,求在这个约束下的最短时间。这就变成一个带约束的优化问题,解析解还能写出来,但已经有点绕了。
我一般会先把两个极端算出来,给决策者一个区间:最快多少秒、最准多少米。中间方案再按需插值。下面这张表是 $d=100$、$v=2$、$u=1$(单位统一)时的对比:
| 方案 | 船头偏角 θ | 横渡时间 | 横向偏移 |
|---|---|---|---|
| 最短时间 | 0° | 50 s | 50 m |
| 最短位移 | 30° | 57.7 s | 0 m |
| 折中(偏移≤20m) | 约 17.5° | 52.4 s | 20 m |
可以看到,为了把偏移从 50 米压到 0,时间只多了不到 8 秒。这个代价在很多场景下是值得的,所以实际工程里最短位移方案用得更多。但如果 $u/v$ 接近 1,比如 0.95,那 $\theta$ 要接近 72°,横渡时间会暴涨到 $d/(v\cos72°) \approx 3.2d/v$,比最短时间慢三倍多,这时候就得掂量了。
2.3 流速不均匀时解析解失效,必须上数值方法
真实河道里流速不是常数。靠近河岸因为摩擦流速小,河中心流速大;弯道外侧流速大于内侧。这时候 $u$ 是 $y$ 的函数 $u(y)$,速度三角形每个位置都不一样,船头朝向需要实时调整,解析公式全部作废。
常见做法是把河道沿 y 方向切成 N 层,每层内近似流速恒定,然后逐层推进。每一层里用速度三角形算出该层的横向漂移和时间增量,累加得到整条轨迹。层数越多越准,但计算量也越大。一般 N 取 100 到 1000 就够工程用了,再多提升有限。
更讲究一点的做法是把它当成最优控制问题:状态量是船的位置 $(x, y)$,控制量是船头角度 $\theta(t)$,动力学是 $\dot{x} = u(y) - v\sin\theta$,$\dot{y} = v\cos\theta$,目标函数是最小化时间或终端横向偏移。这个可以用打靶法或者直接配点法求解,Python 里scipy.optimize就能干。不过对大多数落地场景,分层推进已经足够,代码简单、调试容易、跑得快。
3. 用 Python 把分层推进法跑通:从常流速到变流速
3.1 常流速下的最小可运行代码
先把最简单的常流速情况跑通,验证速度三角形推导没错。这段代码同时算最短时间和最短位移两条轨迹,画在一张图上对比。
import numpy as np import matplotlib.pyplot as plt # 物理参数 d = 100.0 # 河宽,m v = 2.0 # 船在静水中的速度,m/s u = 1.0 # 水流速度,m/s # 最短时间方案:船头正对对岸,theta = 0 t_min = d / v x_drift = u * t_min print(f"最短时间: {t_min:.2f} s, 横向偏移: {x_drift:.2f} m") # 最短位移方案:要求 sin(theta) = u/v theta = np.arcsin(u / v) t_short = d / (v * np.cos(theta)) print(f"最短位移: {t_short:.2f} s, 船头偏角: {np.degrees(theta):.2f}°") # 画两条轨迹 y = np.linspace(0, d, 200) # 最短时间轨迹:x 随 y 线性增长 x1 = (u / v) * y # 最短位移轨迹:x 恒为 0 x2 = np.zeros_like(y) plt.figure(figsize=(6, 8)) plt.plot(x1, y, label=f"最短时间 ({t_min:.1f}s)") plt.plot(x2, y, label=f"最短位移 ({t_short:.1f}s)") plt.xlabel("x (m)") plt.ylabel("y (m)") plt.legend() plt.title("小船渡河:两种方案轨迹对比") plt.grid(True) plt.show()这段代码里theta = np.arcsin(u/v)是关键,它来自 x 方向合速度为零的条件。注意np.arcsin的定义域是 [-1, 1],如果u > v会返回 NaN,程序不会报错但结果没意义。所以实际用的时候一定要先判断u < v,否则要切换到“最小漂移”逻辑,也就是让 $\sin\theta = 1$(船头尽可能偏向上游),此时横向漂移为 $(u - v) \cdot d / \sqrt{v^2 - u^2}$ 之类的表达式,具体推导留到避坑章节讲。
3.2 变流速下的分层推进实现
现在把流速改成随 y 变化的函数,模拟河中心快、两岸慢的情况。用抛物线分布:$u(y) = u_{max} \cdot (1 - ((y - d/2) / (d/2))^2)$,这样两岸流速为零,河中心最大。
import numpy as np import matplotlib.pyplot as plt d = 100.0 v = 2.0 u_max = 1.2 N = 500 # 分层数 def u_of_y(y): """抛物线流速分布,两岸为0,河中心最大""" return u_max * (1 - ((y - d/2) / (d/2))**2) # 最短时间策略:每层船头都正对对岸,theta=0 # 此时 dy/dt = v,所以每层时间 dt = dy / v dy = d / N y_vals = np.linspace(0, d, N+1) x_vals = [0.0] t_total = 0.0 for i in range(N): y_mid = y_vals[i] + dy/2 u_local = u_of_y(y_mid) dt = dy / v t_total += dt x_vals.append(x_vals[-1] + u_local * dt) print(f"变流速最短时间: {t_total:.2f} s, 终端偏移: {x_vals[-1]:.2f} m") # 最短位移策略:每层调整 theta 使 x 方向合速度为零 # 需要 u_local < v,否则该层无法完全抵消 x_vals2 = [0.0] t_total2 = 0.0 for i in range(N): y_mid = y_vals[i] + dy/2 u_local = u_of_y(y_mid) if u_local < v: sin_theta = u_local / v cos_theta = np.sqrt(1 - sin_theta**2) else: # 流速超过船速,只能尽量偏,取 sin_theta = 1 sin_theta = 1.0 cos_theta = 0.0 if cos_theta < 1e-6: # 完全顶不住,横渡分量接近零,视为无法到达 t_total2 = np.inf break dt = dy / (v * cos_theta) t_total2 += dt x_vals2.append(x_vals2[-1] + (u_local - v * sin_theta) * dt) print(f"变流速最短位移: {t_total2:.2f} s, 终端偏移: {x_vals2[-1]:.2f} m") plt.figure(figsize=(6, 8)) plt.plot(x_vals, y_vals, label="最短时间策略") plt.plot(x_vals2, y_vals, label="最短位移策略") plt.xlabel("x (m)") plt.ylabel("y (m)") plt.legend() plt.title("变流速下的两种策略轨迹") plt.grid(True) plt.show()这段代码的核心逻辑是:把河宽切成 N 层,每层内流速用中点值近似。最短时间策略里船头始终正对对岸,所以dt = dy / v是常数,横向漂移累加u_local * dt。最短位移策略里每层都要重新算sin_theta = u_local / v,如果某层流速超过船速,cos_theta会变成零甚至负数,代码里做了保护,把t_total2设为无穷大表示该策略不可行。
参数怎么调:N越大越准,但超过 1000 后对结果影响小于 0.1%,没必要。u_max和v的比值决定问题难度,比值小于 0.5 时两种策略差别不大,大于 0.9 时最短位移策略时间会急剧膨胀。u_of_y可以换成任何你实测的流速剖面,只要保证是 y 的函数就行。
3.3 怎么验证你的仿真没算错
数值方法最怕的是“看起来对但实际错”。三个验证手段我每次都会做:
第一,退化验证。把u_of_y改成常数,看结果是否和解析解一致。上面代码里把u_max设为 1.0 且去掉抛物线形状,最短时间应该是 50 秒、偏移 50 米,最短位移应该是 57.7 秒、偏移 0 米。如果对不上,说明分层逻辑有 bug。
第二,收敛性验证。把 N 从 100 逐步加到 2000,看终端偏移和时间是否收敛到一个稳定值。如果随 N 剧烈波动,说明分层推进的近似有问题,可能是流速函数在层内变化太大。
第三,物理量守恒验证。最短位移策略下,终端横向偏移应该接近零(数值误差范围内)。如果算出个几米的偏移,检查sin_theta的计算是不是漏了u_local随 y 变化的部分。
4. 小船渡河问题避坑:这五个坑我全踩过
4.1 坑一:以为船头方向就是航迹方向
现象:画出来的轨迹和船头指向对不上,明明船头斜着指向上游,轨迹却还在往下游漂。
原因:把 $\vec{v}_b$ 的方向当成了 $\vec{v}$ 的方向。船头方向只决定 $\vec{v}_b$,实际航迹是 $\vec{v}_b + \vec{v}_c$ 的方向。这两个方向只有在流速为零时才重合。
解决:画图时把船头矢量和合速度矢量分开画。船头矢量从船的位置出发,长度正比于 $v$,方向是 $\theta$;合速度矢量同样从船的位置出发,长度正比于 $|\vec{v}|$,方向是 $\arctan(\dot{y}/\dot{x})$。两个箭头不一样就对了。
4.2 坑二:流速超过船速时还在套最短位移公式
现象:程序不报错,但theta算出来是 NaN,或者轨迹直接发散。
原因:np.arcsin(u/v)在u > v时无解。物理意义是船根本压不住水流,不存在垂直横渡的船头角度。
解决:先判断u < v。如果不满足,最短位移方案不存在,只能求“最小漂移”方案:让船头尽可能偏向上游($\sin\theta = 1$),此时横向漂移速度是 $u - v$,横渡速度是零——等等,如果 $\sin\theta = 1$,那 $\cos\theta = 0$,船根本不往前走了。所以严格来说当 $u \ge v$ 时,船无法到达正对岸,只能到达下游某点。工程上这时候要么换更大功率的船,要么选个上游的出发点。
4.3 坑三:分层推进时用层边界流速而不是中点流速
现象:N 取很小的时候结果偏差很大,N 取大了又收敛很慢。
原因:用层边界的流速做近似,相当于用矩形法做积分,误差是一阶的。用中点流速相当于中点法,误差是二阶的,收敛快得多。
解决:始终用y_mid = y_vals[i] + dy/2处的流速。如果流速函数变化剧烈,还可以在层内做一次线性插值,但一般没必要。
4.4 坑四:忘记检查终端是否到达对岸
现象:轨迹画出来只走了一半就停了,或者冲过了对岸还在继续算。
原因:循环条件写成了固定步数,没有判断y >= d就退出。变流速下每层的时间增量不一样,固定步数不一定能正好走到对岸。
解决:用while y < d做循环,最后一步如果超出 d,按比例截断。或者像上面代码那样固定 N 层,但确保y_vals[-1] == d。两种都行,关键是别让船停在河中间。
4.5 坑五:把二维问题简化成一维时丢了横向耦合
现象:只算 y 方向的时间,忽略 x 方向位移对流速的影响。在弯道或者流速随 x 变化时结果完全不对。
原因:真实河道里流速不仅是 y 的函数,也可能是 x 的函数。如果河道是弯的,船在横向漂移过程中会进入不同流速区,形成耦合。
解决:如果流速场是二维的 $u(x, y)$,分层推进法就不够用了,需要改成逐时间步推进:每个时间步根据当前位置查流速,更新船头角度,再更新位置。这就是欧拉法或者龙格库塔法解 ODE。代码会复杂一些,但逻辑更通用。我一般先用分层法快速估算,如果发现流速在横向上变化超过 20%,就切到逐时间步方法。
5. 进阶:把渡河问题变成最优控制,用 scipy 求数值解
前面讲的都是“给定策略算轨迹”,反过来“给定目标求策略”就是最优控制问题。比如:允许的最大横向偏移是 30 米,求最短时间。这个问题用分层法也能做,但需要在外层套一层搜索,比较笨。用scipy.optimize.minimize直接优化船头角度序列更优雅。
思路是把时间分成 M 段,每段内船头角度恒定,优化变量就是这 M 个角度。目标函数是总时间,约束是终端横向偏移不超过 30 米、终端 y 坐标等于河宽。代码如下:
import numpy as np from scipy.optimize import minimize d = 100.0 v = 2.0 u_max = 1.2 M = 20 # 控制段数 x_limit = 30.0 # 允许最大横向偏移 def u_of_y(y): return u_max * (1 - ((y - d/2) / (d/2))**2) def simulate(thetas): """给定 M 个船头角度,返回总时间和终端横向偏移""" y = 0.0 x = 0.0 t = 0.0 # 每段的目标是走 d/M 的 y 距离,但实际 y 速度随 theta 变 # 这里用逐时间步推进更稳 dt = 0.01 seg_idx = 0 y_target = d / M while y < d and t < 1000: if seg_idx < M and y >= y_target * (seg_idx + 1): seg_idx += 1 theta = thetas[min(seg_idx, M-1)] u_local = u_of_y(y) dx = (u_local - v * np.sin(theta)) * dt dy = v * np.cos(theta) * dt x += dx y += dy t += dt return t, x def objective(thetas): t, x = simulate(thetas) # 惩罚项:终端偏移超过限制就加大目标函数 penalty = max(0, abs(x) - x_limit) * 1000 return t + penalty # 初始猜测:全部取最短位移角度 theta0 = np.arcsin(u_max / v) * np.ones(M) * 0.5 res = minimize(objective, theta0, method='Nelder-Mead', options={'maxiter': 2000, 'xatol': 1e-4}) print(f"最优时间: {res.fun:.2f} s") t_opt, x_opt = simulate(res.x) print(f"实际时间: {t_opt:.2f} s, 终端偏移: {x_opt:.2f} m") print(f"各段角度(度): {np.degrees(res.x).round(1)}")这段代码用Nelder-Mead是因为目标函数没有解析梯度,而且变量只有 20 个,无梯度方法够用。simulate里用固定时间步dt=0.01推进,比分层法更通用,能处理任意流速场。惩罚项把横向偏移约束软化进目标函数,1000这个系数是拍脑袋定的,太小约束不住,太大导致优化困难,一般取目标函数量级的 100 到 1000 倍。
跑出来的结果通常是一条“先偏一点、中间修正、最后拉回”的角度序列,和直觉一致。这个方法的代价是计算量大,一次优化要跑几千次仿真,每次仿真几千步,总共几百万次运算,普通笔记本几秒钟能出结果。如果 M 取 50 以上,或者流速场很复杂,可能要几十秒。工程上如果只是离线规划,这个时间可以接受;如果要实时控制,得换成模型预测控制(MPC),每步只优化未来一小段。
一个我踩过的坑:Nelder-Mead对初始猜测敏感,如果theta0全取零,有时候会收敛到局部最优,终端偏移超限。我的习惯是先用分层法算一个粗略的最短位移角度序列作为初始猜测,再交给优化器微调。这样收敛快,结果也稳。
最后说个习惯:每次改完流速函数或者约束,我都会先把x_limit设成无穷大跑一遍,看最短时间是否等于d/v(应该等于,因为船头可以全部正对对岸)。如果不等,说明仿真循环里有 bug。这个检查花不了几秒钟,但能省下大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取