一、马拉松配速问题建模
马拉松全长42.195公里,顶级选手需在2小时左右完成。配速策略直接决定成绩:起跑过快会导致后程”撞墙”,起跑过慢则浪费体能。如何在不同赛段分配速度,是一个经典的最优控制问题。
本文将其转化为动态规划问题:
– 状态:当前位置、剩余能量、已用时间
– 决策:当前赛段的速度
– 约束:最大心率、能量消耗率、肌肉疲劳累积
– 目标:总时间最小化
二、生理能量模型
2.1 能量系统
人体运动依赖三大能量系统:
1. 磷酸原系统(ATP-PC):供能10秒内,瞬间爆发力
2. 糖酵解系统:供能1-2分钟,产生乳酸
3. 有氧氧化系统:马拉松主要供能来源,依赖碳水化合物和脂肪
2.2 核心方程
设:
– E(t):t时刻的有氧能量储备(千焦)
– v(t):瞬时速度(米/秒)
– C(v):单位距离能量消耗 C(v) = a*v + b*v² + c(线性+空气阻力+基础代谢)
能量消耗率:
dE/dt = -C(v) * v = -(a*v² + b*v³ + c*v)
约束条件:
– v_min ≤ v ≤ v_max(最小/最大速度)
– E(t) ≥ 0(能量不能为负)
– v(t) ≤ v_anaerobic(超过此速度依赖无氧供能,乳酸累积)
三、动态规划求解
3.1 离散化建模
将42.195公里分为 N 个赛段(每段1公里),定义状态:
DP[i][e] = 到达第i公里、剩余能量为e时的最短时间
状态转移:
DP[i+1][e'] = min(DP[i][e] + time(i, v))
其中 e' = e - cost(v) * distance
3.2 核心类实现
public class MarathonRunner {
// 生理参数
private double maxEnergy; // 总能量储备 (kJ)
private double vo2Max; // 最大摄氧量 (ml/kg/min)
private double bodyMass; // 体重 (kg)
private double fatigueRate; // 疲劳累积系数
// 能量消耗系数:C(v) = a*v + b*v² + c
private double coeffA; // 跑步经济性系数
private double coeffB; // 空气阻力系数
private double coeffC; // 基础代谢率
public MarathonRunner(double mass, double vo2Max) {
this.bodyMass = mass;
this.vo2Max = vo2Max;
this.maxEnergy = calculateMaxEnergy();
this.fatigueRate = 0.015;
// 典型值(基于生理学研究)
this.coeffA = 0.8; // J/(kg*m) 级别的线性项
this.coeffB = 0.018; // 空气阻力与速度平方成正比
this.coeffC = 3.5; // 基础代谢 (kJ/km/kg)
}
/**
* 根据VO2Max估算总能量储备
* 公式基于 Daniels' Running Formula
*/
private double calculateMaxEnergy() {
// 可使用的碳水化合物约 2000-2500 kJ (肝糖原+肌糖原)
// 加上脂肪氧化(几乎无限,但速率受限)
double carbEnergy = 2200; // kJ
double fatPower = vo2Max * bodyMass * 0.005; // 脂肪供能功率估计
return carbEnergy + fatPower * 7200; // 2小时脂肪供能
}
/**
* 计算单位距离能量消耗 (kJ/km)
*/
public double energyCost(double velocity) {
// velocity in m/s, convert to min/km pace for calculation
double paceMinPerKm = 16.667 / velocity; // 1000m / (v m/s) / 60 = min/km
// 简化模型:能量消耗与速度近似线性,加二次空气阻力项
double cost = coeffC * bodyMass + coeffA * bodyMass * velocity
+ coeffB * velocity * velocity;
return cost / 1000.0; // 转为 kJ/m -> kJ/km 需要乘以1000
}
/**
* 计算在给定速度下跑完距离所需时间
*/
public double timeForDistance(double distance, double velocity) {
return distance / velocity;
}
/**
* 计算最大可持续速度(基于有氧阈值)
*/
public double getAerobicThreshold() {
// 约等于马拉松配速,为VO2Max的 80-85%
double percentVO2 = 0.83;
// 速度(m/s) ≈ VO2 * percent / (coeffA * mass)
return vo2Max * percentVO2 * 0.2; // 简化换算
}
public double getMaxEnergy() { return maxEnergy; }
public double getBodyMass() { return bodyMass; }
public double getVo2Max() { return vo2Max; }
}
3.3 动态规划求解器
import java.util.*;
public class PaceOptimizer {
private static final double SEGMENT_LENGTH = 1000.0; // 每段1公里
private static final double TOTAL_DISTANCE = 42195.0; // 马拉松全长
private static final int NUM_SEGMENTS = 43; // 42.195km -> 43段(最后一段195m)
private static final double ENERGY_DISCRETIZATION = 50.0; // 能量离散化步长(kJ)
private static final double SPEED_DISCRETIZATION = 0.1; // 速度离散化步长(m/s)
private MarathonRunner runner;
private double[][] dp; // dp[segment][energyIndex] = minTime
private int[][] decision; // 记录最优决策
private double[] energyLevels;
private double[] speedLevels;
public PaceOptimizer(MarathonRunner runner) {
this.runner = runner;
initializeDiscretization();
}
private void initializeDiscretization() {
int numEnergyLevels = (int)(runner.getMaxEnergy() / ENERGY_DISCRETIZATION) + 1;
energyLevels = new double[numEnergyLevels];
for (int i = 0; i < numEnergyLevels; i++) {
energyLevels[i] = i * ENERGY_DISCRETIZATION;
}
// 速度范围:2.5 m/s (约6:40/km) 到 6.0 m/s (约2:47/km)
double minSpeed = 2.5, maxSpeed = 6.0;
int numSpeedLevels = (int)((maxSpeed - minSpeed) / SPEED_DISCRETIZATION) + 1;
speedLevels = new double[numSpeedLevels];
for (int i = 0; i < numSpeedLevels; i++) {
speedLevels[i] = minSpeed + i * SPEED_DISCRETIZATION;
}
}
/**
* 动态规划求解最优配速
*/
public OptimalPace solve() {
int numE = energyLevels.length;
dp = new double[NUM_SEGMENTS + 1][numE];
decision = new int[NUM_SEGMENTS][numE];
// 初始化:无穷大
for (int i = 0; i <= NUM_SEGMENTS; i++) {
Arrays.fill(dp[i], Double.MAX_VALUE);
}
// 初始状态:第0段,满能量,时间0
int startE = findEnergyIndex(runner.getMaxEnergy());
dp[0][startE] = 0;
// DP递推
for (int seg = 0; seg < NUM_SEGMENTS; seg++) {
double segDist = (seg == NUM_SEGMENTS - 1) ? 195.0 : SEGMENT_LENGTH;
for (int eIdx = 0; eIdx < numE; eIdx++) {
if (dp[seg][eIdx] == Double.MAX_VALUE) continue;
double currentTime = dp[seg][eIdx];
double currentEnergy = energyLevels[eIdx];
// 尝试所有可能的速度
for (int sIdx = 0; sIdx < speedLevels.length; sIdx++) {
double speed = speedLevels[sIdx];
// 生理约束检查
if (!isPhysiologicallyValid(seg, speed)) continue;
// 计算能量消耗
double energyCost = runner.energyCost(speed) * (segDist / 1000.0);
double remainingEnergy = currentEnergy - energyCost;
if (remainingEnergy < 0) continue; // 能量不足
// 计算时间
double timeCost = segDist / speed;
int nextEIdx = findEnergyIndex(remainingEnergy);
if (currentTime + timeCost < dp[seg + 1][nextEIdx]) {
dp[seg + 1][nextEIdx] = currentTime + timeCost;
decision[seg][eIdx] = sIdx;
}
}
}
}
// 回溯找最优解
return backtrack();
}
/**
* 生理约束检查
*/
private boolean isPhysiologicallyValid(int segment, double speed) {
// 速度不能超过有氧阈值太多(避免乳酸过度累积)
double threshold = runner.getAerobicThreshold();
double maxAllowed = threshold * (1.05 + segment * 0.002); // 前段可稍快
return speed <= maxAllowed && speed >= 2.0;
}
private int findEnergyIndex(double energy) {
int idx = (int)(energy / ENERGY_DISCRETIZATION);
return Math.max(0, Math.min(idx, energyLevels.length - 1));
}
/**
* 回溯重构最优配速方案
*/
private OptimalPace backtrack() {
List<Double> speeds = new ArrayList<>();
List<Double> energies = new ArrayList<>();
// 找最终最短时间
int bestE = 0;
double bestTime = dp[NUM_SEGMENTS][0];
for (int e = 1; e < energyLevels.length; e++) {
if (dp[NUM_SEGMENTS][e] < bestTime) {
bestTime = dp[NUM_SEGMENTS][e];
bestE = e;
}
}
// 回溯
int eIdx = bestE;
for (int seg = NUM_SEGMENTS - 1; seg >= 0; seg--) {
int sIdx = decision[seg][eIdx];
speeds.add(0, speedLevels[sIdx]);
energies.add(0, energyLevels[eIdx]);
// 反向计算上一段能量
double segDist = (seg == NUM_SEGMENTS - 1) ? 195.0 : SEGMENT_LENGTH;
double energyCost = runner.energyCost(speedLevels[sIdx]) * (segDist / 1000.0);
double prevEnergy = energyLevels[eIdx] + energyCost;
eIdx = findEnergyIndex(prevEnergy);
}
return new OptimalPace(speeds, energies, bestTime);
}
static class OptimalPace {
List<Double> segmentSpeeds;
List<Double> segmentEnergies;
double totalTime;
OptimalPace(List<Double> speeds, List<Double> energies, double time) {
this.segmentSpeeds = speeds;
this.segmentEnergies = energies;
this.totalTime = time;
}
public void printPaceChart() {
System.out.println("=== 最优配速方案 ===");
System.out.printf("预计总时间: %.0f秒 (%.2f小时)\n", totalTime, totalTime / 3600);
System.out.println("\n公里 | 配速(min/km) | 速度(m/s) | 剩余能量(kJ)");
System.out.println("-----|-------------|----------|------------");
for (int i = 0; i < segmentSpeeds.size(); i++) {
double speed = segmentSpeeds.get(i);
double pace = 1000.0 / speed / 60.0; // min/km
double energy = segmentEnergies.get(i);
System.out.printf(" %2d | %.2f | %.2f | %.0f\n",
i + 1, pace, speed, energy);
}
}
}
}
3.4 带赛道起伏的增强模型
public class ElevationProfile {
// 赛道海拔数据(每公里)
private double[] elevations;
public ElevationProfile() {
// 模拟典型城市马拉松赛道(轻微起伏)
elevations = new double[]{
10, 12, 15, 18, 20, 22, 25, 28, 30, 32,
35, 38, 40, 42, 45, 48, 50, 52, 55, 58,
60, 58, 55, 52, 50, 48, 45, 42, 40, 38,
35, 32, 30, 28, 25, 22, 20, 18, 15, 12, 10, 8, 5
};
}
/**
* 计算坡度对能量消耗的影响
* 上坡增加消耗,下坡可减少消耗但需控制速度
*/
public double getGradientCost(int segment, double baseCost) {
if (segment >= elevations.length - 1) return baseCost;
double gradient = (elevations[segment + 1] - elevations[segment]) / 1000.0;
// 上坡:每1%坡度增加约 3-4% 能量消耗
// 下坡:节省能量但增加肌肉损伤风险
if (gradient > 0) {
return baseCost * (1 + gradient * 3.5);
} else {
return baseCost * (1 + gradient * 1.5); // 下坡节省较少
}
}
}
3.5 主程序
public class MarathonOptimizer {
public static void main(String[] args) {
// 创建一个典型马拉松选手
// 体重65kg,VO2Max 60 ml/kg/min(精英业余水平)
MarathonRunner runner = new MarathonRunner(65.0, 60.0);
System.out.println("=== 马拉松配速优化器 ===");
System.out.printf("选手参数: %.0fkg, VO2Max=%.0f\n",
runner.getBodyMass(), runner.getVo2Max());
System.out.printf("估算最大能量: %.0f kJ\n", runner.getMaxEnergy());
System.out.printf("有氧阈值速度: %.2f m/s\n\n", runner.getAerobicThreshold());
PaceOptimizer optimizer = new PaceOptimizer(runner);
PaceOptimizer.OptimalPace pace = optimizer.solve();
pace.printPaceChart();
// 对比匀速策略
System.out.println("\n=== 匀速策略对比 ===");
double avgSpeed = 42195.0 / pace.totalTime;
System.out.printf("最优策略平均速度: %.2f m/s (%.2f min/km)\n",
avgSpeed, 1000.0 / avgSpeed / 60.0);
}
}
四、算法复杂度分析
| 参数 | 说明 | 典型值 |
|---|---|---|
| N | 赛段数 | 43 |
| E | 能量离散级别 | ~100 |
| S | 速度离散级别 | ~35 |
动态规划复杂度:
– 时间:O(N × E × S) ≈ 43 × 100 × 35 = 150,500 次运算
– 空间:O(N × E) ≈ 4,300 个状态
属于非常高效的算法,可在毫秒级求解。
五、实际应用与扩展
5.1 实时配速调整
比赛当天可根据实际情况调整:
– 天气因素:高温增加能量消耗10-15%
– 风速:顺风/逆风影响空气阻力项
– 体感:主观疲劳度反馈调整目标配速
5.2 多人竞赛策略
在群体比赛中,加入博弈因素:
– 跟跑策略:利用前方选手破风,减少空气阻力
– 突围时机:在关键赛段(如上坡前)加速甩开对手
六、总结
马拉松配速优化是一个将生理学与运筹学结合的有趣问题。通过动态规划将连续的最优控制问题离散化,我们可以为每位跑者生成个性化的配速方案。核心思想——在约束条件下分配有限资源以优化目标——同样适用于项目管理、财务规划等众多领域。