一、问题介绍与算法目标
铁人三项(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毫秒级完成。
五、关键设计要点
- 功率-速度反函数:利用牛顿迭代法求解非线性方程,将功率约束转化为速度决策
- 疲劳修正功率:疲劳水平越高,实际可用功率越低,形成负反馈约束
- 能量离散化:将连续能量空间均匀离散化,在精度与计算量间取得平衡
- 分段优化:每项运动独立做DP,三项结果串联,降低状态空间维度
- 边界约束:能量和疲劳均限制在[0, max]区间,避免数值溢出
六、扩展方向
- 实时心率约束:引入心率上限,将心血管负荷纳入状态变量
- 赛道地形建模:自行车段加入海拔爬升功率修正(P = P_flat + mgh/t)
- 换项时间优化:在游泳→自行车、自行车→跑步的换项区加入过渡策略
- 风险厌恶策略:用期望值替代确定性模型,处理天气变化等不确定性
- 多运动员博弈:在骑行集团中考虑跟风(drafting)的空气动力学收益