每日算法 — 使用java实现马拉松:分段配速与动态规划能量最优分配

一、马拉松配速问题建模

马拉松全长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 多人竞赛策略

在群体比赛中,加入博弈因素:
– 跟跑策略:利用前方选手破风,减少空气阻力
– 突围时机:在关键赛段(如上坡前)加速甩开对手

六、总结

马拉松配速优化是一个将生理学与运筹学结合的有趣问题。通过动态规划将连续的最优控制问题离散化,我们可以为每位跑者生成个性化的配速方案。核心思想——在约束条件下分配有限资源以优化目标——同样适用于项目管理、财务规划等众多领域。