每日算法 — 使用java实现跨栏比赛:抛物线轨迹与节奏步频优化

一、背景介绍

跨栏(Hurdle Race)是田径运动中的经典项目,运动员需要在跑道上跨越若干个固定间距的栏架,并尽可能快地到达终点。栏架高度(男子 110m 栏 1.067m、女子 100m 栏 0.84m)与栏间距离(男子 9.14m、女子 8.5m)都有国际标准。

本游戏的核心算法包括:

  • 抛物线越栏轨迹:跨栏时身体重心形成的抛物线必须满足”最高点 > 栏高 + 安全裕度”。
  • 步频节奏优化:在保证越过栏架的前提下,找到最优的步数分配。
  • 能量消耗模型:跑步消耗与速度呈非线性关系(空气阻力、内部能量消耗)。
  • 步长约束:每步的长度受运动员腿长与爆发力限制。

二、整体设计

  • Athlete:包含体重、腿长、最大速度、当前体力。
  • Hurdle:表示一个栏架,位置、高度、宽度。
  • TrajectoryCalculator:计算跨过栏架所需的最小垂直初速度与水平速度。
  • RhythmOptimizer:在固定栏间距下枚举步数分配,求解最快方案。
  • GameMain:演示 110 米栏 10 个栏架的最优节奏方案。

三、完整代码实现

3.1 运动员 Athlete.java

/**
 * 运动员参数
 * 体重 m、腿长 L、最大速度 vMax
 * 能量消耗 E = a * v^2 + b * v + c
 */
public class Athlete {
    public final String name;
    public final double mass;       // kg
    public final double legLength;  // m
    public final double maxSpeed;   // m/s
    public final double energyA, energyB, energyC;

    public Athlete(String name, double mass, double legLength, double maxSpeed) {
        this.name = name;
        this.mass = mass;
        this.legLength = legLength;
        this.maxSpeed = maxSpeed;
        // 经验参数
        this.energyA = 0.5;
        this.energyB = 0.3;
        this.energyC = 0.05;
    }

    /**
     * 速度 v 下的能量消耗率 (W/kg)
     */
    public double energyRate(double v) {
        return energyA * v * v + energyB * v + energyC;
    }

    /**
     * 最大可能步长(腿长约束)
     */
    public double maxStepLength() {
        return legLength * 1.4;
    }

    /**
     * 最小可能步长(避免步幅过小导致失衡)
     */
    public double minStepLength() {
        return legLength * 0.9;
    }
}

3.2 栏架 Hurdle.java

/**
 * 栏架
 * 包含位置 x、高度 h、宽度 w
 */
public class Hurdle {
    public final double x;  // 距离起跑线位置(m)
    public final double h;  // 高度(m)
    public final double w;  // 宽度(m)

    public Hurdle(double x, double h, double w) {
        this.x = x; this.h = h; this.w = w;
    }

    @Override
    public String toString() {
        return String.format("Hurdle(x=%.2f, h=%.3f)", x, h);
    }
}

3.3 抛物线轨迹计算 TrajectoryCalculator.java

/**
 * 抛物线轨迹计算
 * 已知水平距离 d、栏高 h、水平速度 vx,
 * 求最小垂直初速度 vy,使得:
 *   1) y(t) = vy * t - 0.5 * g * t^2  在 x 方向上达到 d
 *   2) 顶点 yMax = vy^2 / (2g) >= h + safeMargin
 *
 * 解法:
 *   t = d / vx
 *   vy = (h + safeMargin) * 2g / (2 * (h + safeMargin)) ... 由顶点条件解
 *   实际上 vy = sqrt(2g(h + safeMargin))
 *   再求落地时间 tEnd = 2 * vy / g,总飞行距离 = vx * tEnd
 */
public class TrajectoryCalculator {
    public static final double G = 9.8;
    public static final double SAFE_MARGIN = 0.10; // 10 cm 净空

    /**
     * 给定水平速度 vx 与栏高 h,求最小垂直初速度 vy
     * 返回 vy(m/s)
     */
    public static double minVerticalVelocity(double h) {
        return Math.sqrt(2 * G * (h + SAFE_MARGIN));
    }

    /**
     * 给定水平速度 vx 与栏间距 d,求过栏时间
     */
    public static double timeOverHurdle(double vx, double d) {
        return d / vx;
    }

    /**
     * 跨栏能量消耗估算:起跳 + 凌空 + 落地
     * 简化模型:E = 0.5 * m * vy^2 + 0.5 * m * vx^2 * (m/h_bar) + 落地缓冲
     */
    public static double hurdleEnergyCost(double m, double vx, double h) {
        double vy = minVerticalVelocity(h);
        return 0.5 * m * vy * vy + 0.5 * m * vx * vx * 0.2 + 50;
    }
}

3.4 节奏优化器 RhythmOptimizer.java

import java.util.*;

/**
 * 节奏优化器
 * 目标:在固定栏数与栏间距下,找到最少的总时间
 * 决策变量:每两个栏之间用几步 (stepCount)
 * 约束:
 *   1) 每步长度在 [minStep, maxStep] 范围内
 *   2) 步数必须是正整数
 *   3) 跨栏时水平速度受限于最大速度
 *
 * 算法:动态规划
 *   dp[i][k] = 到达第 i 个栏用了 k 步的最小时间
 *   转移:枚举第 i-1 到第 i 之间使用的步数
 * 时间复杂度:O(N * totalSteps * maxStepCount)
 */
public class RhythmOptimizer {
    private final List<Hurdle> hurdles;
    private final Athlete athlete;
    private final double startToFirst;
    private final double lastToFinish;

    public RhythmOptimizer(Athlete athlete, List<Hurdle> hurdles,
                           double startToFirst, double lastToFinish) {
        this.athlete = athlete;
        this.hurdles = hurdles;
        this.startToFirst = startToFirst;
        this.lastToFinish = lastToFinish;
    }

    /**
     * 求解最优节奏
     * 返回总时间与每段步数
     */
    public OptimizationResult optimize() {
        int n = hurdles.size();
        // segment 长度:起点->hurdle[0], hurdle[0]->hurdle[1], ..., hurdle[n-1]->终点
        double[] segments = new double[n + 1];
        segments[0] = startToFirst;
        for (int i = 1; i < n; i++) segments[i] = hurdles.get(i).x - hurdles.get(i - 1).x;
        segments[n] = lastToFinish;

        // 最大可能步数 = ceil(segments / minStep) + 1
        int maxSteps = 0;
        for (double s : segments) {
            int steps = (int) Math.ceil(s / athlete.minStepLength()) + 1;
            maxSteps = Math.max(maxSteps, steps);
        }

        // dp[i][k] = 到达 segment i 末尾用了 k 步的最小时间
        // i = 0..n,k = 0..maxSteps
        double[][] dp = new double[n + 1][maxSteps + 1];
        int[][] choice = new int[n + 1][maxSteps + 1]; // 记录该段用了多少步
        for (double[] row : dp) Arrays.fill(row, Double.POSITIVE_INFINITY);
        dp[0][0] = 0;

        for (int i = 0; i <= n; i++) {
            for (int k = 0; k <= maxSteps; k++) {
                if (dp[i][k] == Double.POSITIVE_INFINITY) continue;
                // 枚举下一段步数
                for (int sk = 1; sk <= maxSteps - k; sk++) {
                    int totalK = k + sk;
                    double segLen = segments[i];
                    double stepLen = segLen / sk;
                    if (stepLen < athlete.minStepLength() || stepLen > athlete.maxStepLength())
                        continue;
                    double v = stepLen * 4.0; // 假设每步 0.25s
                    v = Math.min(v, athlete.maxSpeed);
                    double t = sk * 0.25;
                    // 跨栏额外时间(仅对有栏的段)
                    double extra = 0;
                    if (i >= 1 && i <= n - 1) {
                        extra = 0.05; // 凌空 + 落地额外 50ms
                    }
                    double newTime = dp[i][k] + t + extra;
                    if (newTime < dp[i + 1][totalK]) {
                        dp[i + 1][totalK] = newTime;
                        choice[i + 1][totalK] = sk;
                    }
                }
            }
        }

        // 找最佳终点
        double bestTime = Double.POSITIVE_INFINITY;
        int bestK = 0;
        for (int k = 0; k <= maxSteps; k++) {
            if (dp[n][k] < bestTime) { bestTime = dp[n][k]; bestK = k; }
        }

        // 回溯步数
        int[] stepsPerSegment = new int[n + 1];
        int curK = bestK;
        for (int i = n; i >= 1; i--) {
            stepsPerSegment[i] = choice[i][curK];
            curK -= choice[i][curK];
        }
        return new OptimizationResult(bestTime, stepsPerSegment, segments);
    }

    public static class OptimizationResult {
        public final double totalTime;
        public final int[] stepsPerSegment;
        public final double[] segmentLengths;

        public OptimizationResult(double totalTime, int[] steps, double[] segs) {
            this.totalTime = totalTime;
            this.stepsPerSegment = steps;
            this.segmentLengths = segs;
        }

        public void print() {
            System.out.printf("Total time: %.3f s%n", totalTime);
            for (int i = 0; i < stepsPerSegment.length; i++) {
                double len = segmentLengths[i];
                double stepLen = len / stepsPerSegment[i];
                System.out.printf("  Segment %2d: len=%.2fm  steps=%d  stepLen=%.2fm%n",
                        i, len, stepsPerSegment[i], stepLen);
            }
        }
    }
}

3.5 主程序 GameMain.java

import java.util.*;

/**
 * 跨栏比赛节奏优化演示
 * 模拟男子 110 米栏:10 个栏,栏高 1.067m,栏间距 9.14m
 */
public class GameMain {
    public static void main(String[] args) {
        Athlete bolt = new Athlete("Athlete", 80, 1.05, 11.5);

        // 10 个栏,栏间 9.14m,栏高 1.067m
        List<Hurdle> hurdles = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            hurdles.add(new Hurdle(13.72 + 9.14 * (i - 1), 1.067, 1.2));
        }
        double startToFirst = hurdles.get(0).x; // 13.72 m
        double lastToFinish = 110.0 - hurdles.get(9).x; // 14.02 m

        RhythmOptimizer opt = new RhythmOptimizer(bolt, hurdles, startToFirst, lastToFinish);
        RhythmOptimizer.OptimizationResult res = opt.optimize();
        System.out.println("=== 110米跨栏节奏优化 ===");
        res.print();
    }
}

四、算法分析

4.1 时间复杂度

  • 设段数 N(= 栏数 + 1),单段最大步数 M,DP 状态 O(N*M),转移 O(M)。
  • 总时间复杂度 O(N * M²)。
  • 在 N=11(10栏+起点段+终点段)、M=8 时约 700 次运算。

4.2 物理参数

  • 重力 G = 9.8 m/s²
  • 安全裕度 10 cm
  • 每步时间 0.25 s

4.3 模型精度

  • 简化的能量模型假设每步 0.25 s,未考虑加速度段与匀速段切换。
  • 实际跨栏还需考虑起跨距离(栏前 2.0-2.2m)与下栏距离(栏后 1.0-1.4m)。

五、可扩展方向

  1. 连续优化:使用梯度下降优化步长而非离散步数。
  2. 真实生物力学模型:引入髋关节、膝关节角度约束。
  3. 疲劳累积:随着比赛进行,能量消耗率上升,最大速度下降。
  4. 天气因素:风向、湿度影响空气阻力与能量消耗。

六、运行说明

将 Athlete.java、Hurdle.java、TrajectoryCalculator.java、RhythmOptimizer.java、GameMain.java 放入同一目录,javac *.java && java GameMain,程序会输出 110 米跨栏 11 段的最优步数分配。