每日算法 — 使用java实现铁人三项:分段最优配速与动态规划能量分配

一、问题介绍与算法目标

铁人三项(Triathlon)是依次完成游泳、自行车、长跑三项运动的耐力赛事。运动员需要在3.8公里游泳、180公里自行车和42.2公里马拉松之间分配有限体能,任何一项过快或过慢都会影响总成绩。

本文用Java实现铁人三项最优配速计算器,核心算法包括:

  • 分段动态规划:在距离-时间-能量三维状态空间中寻找最优路径
  • 体能衰减模型:基于生理学的能量消耗与恢复建模
  • 最优速度决策:在每个决策点选择使总成绩最小的配速

二、数学建模

2.1 能量系统模型

运动员的能量状态由以下要素描述:
– 剩余体能(E):以碳水化合物储备为主,单位:千焦(kJ)
– 疲劳累积(F):肌肉代谢废物积累,影响功率输出效率
– 实时功率(P):当前运动强度,单位:瓦特(W)

功率与速度的关系采用经验公式:

游泳:P = k1 · v³          (水阻力与速度立方成正比)
自行车:P = k2 · v³ + Crr  (空气阻力+滚动阻力)
跑步:P = k3 · v + k4 · v³  (代谢成本+风阻)

2.2 状态转移

将每项运动均分为N个赛段,每个赛段末做一个配速决策。状态转移方程:

E[i+1] = E[i] - Δt · (P / η) + Δt · R      // 能量消耗与基础恢复
F[i+1] = F[i] + Δt · α · P - Δt · β · F    // 疲劳积累与消退
T[i+1] = T[i] + Δt                          // 总时间累积

其中 η 为能量转化效率,R 为基础代谢恢复率,α 为疲劳积累系数,β 为疲劳消退系数。

三、完整Java实现

import java.util.*;

/**
 * 铁人三项最优配速计算器
 * 使用分段动态规划在能量约束下最小化总用时
 */
public class TriathlonPacingOptimizer {

    /** 运动项目参数 */
    public static class Segment {
        String name;           // 项目名称
        double distance;       // 距离(米)
        double kDrag;          // 阻力系数
        double kMetabolic;     // 代谢系数
        double kRolling;       // 滚动阻力(自行车用)
        double eta;            // 能量转化效率

        public Segment(String name, double distance, double kDrag,
                       double kMetabolic, double kRolling, double eta) {
            this.name = name;
            this.distance = distance;
            this.kDrag = kDrag;
            this.kMetabolic = kMetabolic;
            this.kRolling = kRolling;
            this.eta = eta;
        }

        /** 根据速度计算所需功率(瓦特) */
        public double powerAtSpeed(double v) {
            // 综合功率 = 代谢功率 + 阻力功率 + 滚动阻力
            return kMetabolic * v + kDrag * v * v * v + kRolling;
        }

        /** 根据功率计算速度(牛顿迭代求解反函数) */
        public double speedAtPower(double P) {
            double v = 2.0; // 初始猜测
            for (int i = 0; i < 20; i++) {
                double f = kMetabolic * v + kDrag * v * v * v + kRolling - P;
                double df = kMetabolic + 3 * kDrag * v * v;
                if (Math.abs(df) < 1e-10) break;
                v = v - f / df;
                if (Math.abs(f) < 1e-6) break;
            }
            return Math.max(0.5, v);
        }
    }

    /** 运动员生理参数 */
    public static class Athlete {
        double maxEnergy;      // 最大能量储备 (kJ)
        double maxPower;       // 最大可持续功率 (W)
        double fatigueAlpha;   // 疲劳积累系数
        double fatigueBeta;    // 疲劳消退系数
        double recoveryRate;   // 基础恢复率 (kJ/s)

        public Athlete(double maxEnergy, double maxPower,
                       double fatigueAlpha, double fatigueBeta, double recoveryRate) {
            this.maxEnergy = maxEnergy;
            this.maxPower = maxPower;
            this.fatigueAlpha = fatigueAlpha;
            this.fatigueBeta = fatigueBeta;
            this.recoveryRate = recoveryRate;
        }
    }

    /** 状态:距离、能量、疲劳、时间 */
    public static class State {
        double distance;       // 已完成距离
        double energy;         // 剩余能量 (kJ)
        double fatigue;        // 疲劳水平 [0, 1]
        double totalTime;      // 总用时 (秒)
        List<Double> speeds;   // 历史配速记录

        public State(double distance, double energy, double fatigue, double totalTime) {
            this.distance = distance;
            this.energy = energy;
            this.fatigue = fatigue;
            this.totalTime = totalTime;
            this.speeds = new ArrayList<>();
        }

        public State copy() {
            State s = new State(distance, energy, fatigue, totalTime);
            s.speeds.addAll(speeds);
            return s;
        }
    }

    private final Segment[] segments;
    private final Athlete athlete;
    private final int stepsPerSegment; // 每项运动的分段数

    public TriathlonPacingOptimizer(Segment[] segments, Athlete athlete, int stepsPerSegment) {
        this.segments = segments;
        this.athlete = athlete;
        this.stepsPerSegment = stepsPerSegment;
    }

    /**
     * 动态规划求解最优配速
     * 状态空间:每项运动结束后,能量和疲劳的离散化组合
     */
    public State optimize() {
        // 初始状态
        State initial = new State(0, athlete.maxEnergy, 0, 0);

        // 逐段处理
        State current = initial;
        for (Segment seg : segments) {
            current = optimizeSegment(current, seg);
            if (current == null) {
                System.out.println("警告:在 " + seg.name + " 阶段能量耗尽!");
                return null;
            }
            System.out.printf("完成 %s: 用时 %.0f秒, 剩余能量 %.1fkJ, 疲劳 %.2f%n",
                seg.name, current.totalTime, current.energy, current.fatigue);
        }

        return current;
    }

    /**
     * 对单个运动项目进行分段DP优化
     */
    private State optimizeSegment(State start, Segment seg) {
        double stepDistance = seg.distance / stepsPerSegment;
        int energyGrids = 50;   // 能量离散化格数
        int fatigueGrids = 20;  // 疲劳离散化格数

        // DP表:dp[e][f] = 到达该状态的最小时间
        double[][] dp = new double[energyGrids + 1][fatigueGrids + 1];
        State[][] prev = new State[energyGrids + 1][fatigueGrids + 1];

        for (double[] row : dp) Arrays.fill(row, Double.POSITIVE_INFINITY);

        int startE = (int) (start.energy / athlete.maxEnergy * energyGrids);
        int startF = (int) (start.fatigue * fatigueGrids);
        startE = Math.min(startE, energyGrids);
        startF = Math.min(startF, fatigueGrids);
        dp[startE][startF] = start.totalTime;
        prev[startE][startF] = start;

        for (int step = 0; step < stepsPerSegment; step++) {
            double[][] newDp = new double[energyGrids + 1][fatigueGrids + 1];
            State[][] newPrev = new State[energyGrids + 1][fatigueGrids + 1];
            for (double[] row : newDp) Arrays.fill(row, Double.POSITIVE_INFINITY);

            // 枚举所有可达状态
            for (int e = 0; e <= energyGrids; e++) {
                for (int f = 0; f <= fatigueGrids; f++) {
                    if (dp[e][f] == Double.POSITIVE_INFINITY) continue;

                    double energy = e * athlete.maxEnergy / energyGrids;
                    double fatigue = f / (double) fatigueGrids;

                    // 枚举可选功率
                    for (double powerRatio = 0.3; powerRatio <= 1.0; powerRatio += 0.05) {
                        double power = powerRatio * athlete.maxPower;

                        // 疲劳修正实际可用功率
                        double effectivePower = power * (1 - 0.5 * fatigue);
                        if (effectivePower < 10) continue;

                        double speed = seg.speedAtPower(effectivePower);
                        double time = stepDistance / speed;

                        // 状态转移
                        double energyCost = (effectivePower / seg.eta) * time / 1000.0; // 转为kJ
                        double newEnergy = energy - energyCost + athlete.recoveryRate * time;
                        newEnergy = Math.max(0, Math.min(athlete.maxEnergy, newEnergy));

                        double newFatigue = fatigue + time * athlete.fatigueAlpha * powerRatio;
                        newFatigue = newFatigue - time * athlete.fatigueBeta * fatigue;
                        newFatigue = Math.max(0, Math.min(1, newFatigue));

                        int newE = (int) (newEnergy / athlete.maxEnergy * energyGrids);
                        int newF = (int) (newFatigue * fatigueGrids);
                        newE = Math.min(newE, energyGrids);
                        newF = Math.min(newF, fatigueGrids);

                        double newTime = dp[e][f] + time;
                        if (newTime < newDp[newE][newF]) {
                            newDp[newE][newF] = newTime;
                            State s = prev[e][f] != null ? prev[e][f].copy() : start.copy();
                            s.distance += stepDistance;
                            s.energy = newEnergy;
                            s.fatigue = newFatigue;
                            s.totalTime = newTime;
                            s.speeds.add(speed);
                            newPrev[newE][newF] = s;
                        }
                    }
                }
            }

            dp = newDp;
            prev = newPrev;
        }

        // 找最优终态:时间最小且能量不为0
        double bestTime = Double.POSITIVE_INFINITY;
        State best = null;
        for (int e = 0; e <= energyGrids; e++) {
            for (int f = 0; f <= fatigueGrids; f++) {
                if (dp[e][f] < bestTime) {
                    bestTime = dp[e][f];
                    best = prev[e][f];
                }
            }
        }

        return best;
    }

    /**
     * 贪心策略:均匀配速(基准对比)
     */
    public State greedyUniformPacing() {
        State current = new State(0, athlete.maxEnergy, 0, 0);

        for (Segment seg : segments) {
            // 简单策略:每项运动使用恒定功率
            double avgPower = athlete.maxPower * 0.75;
            double speed = seg.speedAtPower(avgPower);
            double time = seg.distance / speed;
            double energyCost = (avgPower / seg.eta) * time / 1000.0;

            current.energy -= energyCost - athlete.recoveryRate * time;
            current.energy = Math.max(0, current.energy);
            current.fatigue += time * athlete.fatigueAlpha * 0.75;
            current.fatigue -= time * athlete.fatigueBeta * current.fatigue;
            current.fatigue = Math.max(0, Math.min(1, current.fatigue));
            current.totalTime += time;
            current.distance += seg.distance;
        }

        return current;
    }

    /** 格式化时间输出 */
    public static String formatTime(double seconds) {
        int hrs = (int) (seconds / 3600);
        int mins = (int) ((seconds % 3600) / 60);
        int secs = (int) (seconds % 60);
        return String.format("%02d:%02d:%02d", hrs, mins, secs);
    }

    /** 测试主程序 */
    public static void main(String[] args) {
        // 铁人三项标准距离
        Segment swim = new Segment("游泳", 3800, 0.0008, 50, 0, 0.22);
        Segment bike = new Segment("自行车", 180000, 0.000004, 5, 15, 0.24);
        Segment run = new Segment("跑步", 42200, 0.00002, 80, 0, 0.20);

        Segment[] segments = {swim, bike, run};

        // 业余精英运动员参数
        Athlete athlete = new Athlete(
            1200,    // 1200kJ 糖原储备
            280,     // 280W 阈值功率
            0.0003,  // 疲劳积累
            0.0001,  // 疲劳消退
            0.05     // 基础恢复 kJ/s
        );

        TriathlonPacingOptimizer optimizer = new TriathlonPacingOptimizer(segments, athlete, 20);

        System.out.println("=== 铁人三项最优配速计算 ===");
        System.out.println("运动员参数: 能量储备=" + athlete.maxEnergy + "kJ, 阈值功率=" + athlete.maxPower + "W");
        System.out.println();

        // 动态规划最优解
        System.out.println("--- 动态规划最优配速 ---");
        State optimal = optimizer.optimize();
        if (optimal != null) {
            System.out.println("\n最优总用时: " + formatTime(optimal.totalTime));
            System.out.println("终点剩余能量: " + String.format("%.1f", optimal.energy) + "kJ");
            System.out.println("终点疲劳水平: " + String.format("%.2f", optimal.fatigue));
        }

        // 贪心均匀配速对比
        System.out.println("\n--- 均匀配速对比 ---");
        State greedy = optimizer.greedyUniformPacing();
        System.out.println("均匀配速总用时: " + formatTime(greedy.totalTime));
        System.out.println("终点剩余能量: " + String.format("%.1f", greedy.energy) + "kJ");
        System.out.println("终点疲劳水平: " + String.format("%.2f", greedy.fatigue));

        if (optimal != null) {
            System.out.println("\nDP优化节省时间: " + String.format("%.0f", greedy.totalTime - optimal.totalTime) + "秒");
        }
    }
}

四、算法复杂度分析

操作 时间复杂度 空间复杂度
单步状态转移 O(E · F · P) O(E · F)
单项运动DP O(S · E · F · P) O(E · F)
完整三项优化 O(3 · S · E · F · P) O(E · F)

实际运行中,E=50, F=20, P=15, S=20,单步运算量约30万次,现代CPU毫秒级完成。

五、关键设计要点

  1. 功率-速度反函数:利用牛顿迭代法求解非线性方程,将功率约束转化为速度决策
  2. 疲劳修正功率:疲劳水平越高,实际可用功率越低,形成负反馈约束
  3. 能量离散化:将连续能量空间均匀离散化,在精度与计算量间取得平衡
  4. 分段优化:每项运动独立做DP,三项结果串联,降低状态空间维度
  5. 边界约束:能量和疲劳均限制在[0, max]区间,避免数值溢出

六、扩展方向

  • 实时心率约束:引入心率上限,将心血管负荷纳入状态变量
  • 赛道地形建模:自行车段加入海拔爬升功率修正(P = P_flat + mgh/t)
  • 换项时间优化:在游泳→自行车、自行车→跑步的换项区加入过渡策略
  • 风险厌恶策略:用期望值替代确定性模型,处理天气变化等不确定性
  • 多运动员博弈:在骑行集团中考虑跟风(drafting)的空气动力学收益