每日算法 — 使用java实现投篮:抛体运动学建模与梯度下降最优角度

投篮是体育运动中最经典的抛物线运动场景。与简单的平抛或斜抛不同,真实投篮需要考虑出手高度、篮筐高度、水平距离,以及篮球与篮筐之间的几何碰撞关系。本文用Java实现投篮物理模拟,并运用梯度下降算法求解不同距离下的最优出手角度,帮助理解运动学方程与数值优化在实际问题中的结合。

一、投篮物理模型

将篮球视为质点,忽略空气阻力,其运动遵循标准抛体运动方程。设出手位置为原点水平参考点,出手高度为 (h_0),篮筐高度为 (H),水平距离为 (L),出手角度为 (\theta),初速度为 (v_0)。

水平位移:(x(t) = v_0 \cos\theta \cdot t)

竖直位移:(y(t) = h_0 + v_0 \sin\theta \cdot t – \frac{1}{2}gt^2)

篮球入筐的关键约束是:当 (x = L) 时,(y = H) 且竖直速度方向向下(即球从上方落入篮筐)。通过约束条件可建立角度与初速度的关系,进而定义”入筐误差”作为优化目标。

二、核心算法设计

2.1 入筐条件判定

给定出手角度 (\theta) 和初速度 (v_0),计算篮球到达篮筐水平位置时的竖直高度。若该高度等于篮筐高度,且此时竖直速度为负(向下),则判定为成功入筐。

2.2 最优角度搜索

对于固定的初速度 (v_0),存在两个理论入筐角度(高抛和低抛)。本文采用梯度下降法,以出手角度为变量,以”到达篮筐位置时的竖直偏差”为损失函数,迭代收敛到最优解。损失函数对角度的导数可通过自动微分或解析求导获得。

2.3 篮板碰撞补充

在近距离投篮时,篮球可能先击中篮板再反弹入筐。本文额外实现了基于反射定律的篮板碰撞检测:入射角等于反射角,速度大小按弹性系数衰减。

三、完整Java实现

import java.util.ArrayList;
import java.util.List;

/**
 * 投篮物理模拟与最优角度求解系统
 * 运用抛体运动学方程与梯度下降算法
 */
public class BasketballShot {

    // 物理常量
    private static final double G = 9.8;           // 重力加速度 (m/s^2)
    private static final double HOOP_HEIGHT = 3.05; // 篮筐标准高度 (m)
    private static final double HOOP_RADIUS = 0.225; // 篮筐半径 (m)
    private static final double BALL_RADIUS = 0.12;  // 篮球半径 (m)
    private static final double ELASTICITY = 0.75;   // 篮板弹性系数

    // 场景参数
    private final double shooterHeight;  // 出手高度 (m)
    private final double distance;       // 水平距离 (m)
    private final double backboardX;     // 篮板水平位置 (m),若小于0则无篮板
    private final double backboardY;     // 篮板中心高度 (m)

    public BasketballShot(double shooterHeight, double distance,
                          double backboardX, double backboardY) {
        this.shooterHeight = shooterHeight;
        this.distance = distance;
        this.backboardX = backboardX;
        this.backboardY = backboardY;
    }

    /**
     * 轨迹点记录
     */
    public static class TrajectoryPoint {
        public final double t, x, y, vx, vy;
        public TrajectoryPoint(double t, double x, double y, double vx, double vy) {
            this.t = t; this.x = x; this.y = y; this.vx = vx; this.vy = vy;
        }
    }

    /**
     * 模拟篮球飞行轨迹,支持篮板碰撞检测
     * @param v0 初速度 (m/s)
     * @param angleDeg 出手角度 (度)
     * @return 轨迹点列表
     */
    public List<TrajectoryPoint> simulate(double v0, double angleDeg) {
        List<TrajectoryPoint> path = new ArrayList<>();
        double angleRad = Math.toRadians(angleDeg);
        double vx = v0 * Math.cos(angleRad);
        double vy = v0 * Math.sin(angleRad);
        double x = 0.0;
        double y = shooterHeight;
        double t = 0.0;
        double dt = 0.001; // 1ms时间步长

        boolean hasRebounded = false;

        while (y >= 0 && x <= distance + 2.0 && t < 10.0) {
            path.add(new TrajectoryPoint(t, x, y, vx, vy));

            // 检测篮板碰撞(仅一次)
            if (!hasRebounded && backboardX > 0
                && Math.abs(x - backboardX) < BALL_RADIUS
                && y > backboardY - 0.3 && y < backboardY + 0.3) {
                vx = -vx * ELASTICITY; // 水平速度反向并衰减
                vy = vy * ELASTICITY;
                hasRebounded = true;
            }

            // 检测是否已越过篮筐位置
            if (x >= distance && !hasRebounded) break;

            // 欧拉积分更新状态
            x += vx * dt;
            y += vy * dt;
            vy -= G * dt;
            t += dt;
        }
        return path;
    }

    /**
     * 计算给定角度下的入筐误差损失
     * 损失 = (到达篮筐水平位置时的竖直高度 - 篮筐高度)^2
     */
    public double loss(double v0, double angleDeg) {
        double angleRad = Math.toRadians(angleDeg);
        double vx = v0 * Math.cos(angleRad);
        double vy = v0 * Math.sin(angleRad);

        // 若水平速度为0,返回极大损失
        if (Math.abs(vx) < 1e-9) return 1e9;

        double tArrival = distance / vx;
        double yAtHoop = shooterHeight + vy * tArrival
                        - 0.5 * G * tArrival * tArrival;

        // 入筐时球心需高于篮筐前沿、低于篮筐后沿(考虑球半径)
        double yError = yAtHoop - HOOP_HEIGHT;
        return yError * yError;
    }

    /**
     * 损失函数对角度的数值导数
     */
    public double lossDerivative(double v0, double angleDeg) {
        double h = 1e-6;
        return (loss(v0, angleDeg + h) - loss(v0, angleDeg - h)) / (2 * h);
    }

    /**
     * 梯度下降求解最优出手角度
     * @param v0 固定初速度
     * @param initialAngle 初始猜测角度
     * @param learningRate 学习率
     * @param maxIter 最大迭代次数
     * @return 最优角度(度)
     */
    public double findOptimalAngle(double v0, double initialAngle,
                                    double learningRate, int maxIter) {
        double angle = initialAngle;
        for (int i = 0; i < maxIter; i++) {
            double grad = lossDerivative(v0, angle);
            double newAngle = angle - learningRate * grad;

            // 限制角度在合理范围 [10, 85] 度
            newAngle = Math.max(10.0, Math.min(85.0, newAngle));

            if (Math.abs(newAngle - angle) < 1e-7) {
                System.out.println("  梯度下降收敛于第 " + (i + 1) + " 次迭代");
                break;
            }
            angle = newAngle;
        }
        return angle;
    }

    /**
     * 解析法求解理论入筐角度(无篮板情况)
     * 利用约束方程推导:y(L) = H 求解 theta
     */
    public double[] analyticalAngles(double v0) {
        double dy = HOOP_HEIGHT - shooterHeight;
        double discriminant = v0 * v0 * v0 * v0
                - G * (G * distance * distance + 2 * dy * v0 * v0);

        if (discriminant < 0) return new double[0]; // 速度不足

        double tan1 = (v0 * v0 + Math.sqrt(discriminant)) / (G * distance);
        double tan2 = (v0 * v0 - Math.sqrt(discriminant)) / (G * distance);

        return new double[] {
            Math.toDegrees(Math.atan(tan1)),
            Math.toDegrees(Math.atan(tan2))
        };
    }

    /**
     * 判定投篮结果
     */
    public String judgeShot(double v0, double angleDeg) {
        List<TrajectoryPoint> path = simulate(v0, angleDeg);
        TrajectoryPoint last = path.get(path.size() - 1);

        // 检查是否经过篮筐附近
        for (TrajectoryPoint p : path) {
            if (Math.abs(p.x - distance) < 0.05
                && p.y >= HOOP_HEIGHT - BALL_RADIUS
                && p.y <= HOOP_HEIGHT + HOOP_RADIUS
                && p.vy < 0) {
                return "进球!角度=" + String.format("%.2f", angleDeg)
                     + "°, 初速=" + String.format("%.2f", v0) + "m/s";
            }
        }
        return "未进球。落点高度=" + String.format("%.3f", last.y) + "m";
    }

    // ==================== 主程序 ====================
    public static void main(String[] args) {
        System.out.println("===== 投篮最优角度求解系统 =====\n");

        // 场景1:罚球线投篮(距离4.225m,无篮板碰撞)
        System.out.println("【场景1】罚球线投篮,距离4.225m");
        BasketballShot freeThrow = new BasketballShot(2.0, 4.225, -1, 0);
        double v0 = 8.0;
        double optAngle = freeThrow.findOptimalAngle(v0, 45.0, 0.5, 1000);
        System.out.println("  固定初速 " + v0 + "m/s,梯度下降最优角度: "
                         + String.format("%.4f", optAngle) + "°");
        double[] theory = freeThrow.analyticalAngles(v0);
        System.out.println("  解析解验证: 高抛=" + String.format("%.4f", theory[0])
                         + "°, 低抛=" + String.format("%.4f", theory[1]) + "°");
        System.out.println("  " + freeThrow.judgeShot(v0, optAngle));

        // 场景2:三分线投篮(距离6.75m)
        System.out.println("\n【场景2】三分线投篮,距离6.75m");
        BasketballShot threePoint = new BasketballShot(2.0, 6.75, -1, 0);
        v0 = 9.5;
        optAngle = threePoint.findOptimalAngle(v0, 50.0, 0.4, 1000);
        System.out.println("  固定初速 " + v0 + "m/s,梯度下降最优角度: "
                         + String.format("%.4f", optAngle) + "°");
        System.out.println("  " + threePoint.judgeShot(v0, optAngle));

        // 场景3:近距离打板投篮(距离2.5m,有篮板)
        System.out.println("\n【场景3】近距离打板投篮,距离2.5m,篮板在1.5m处");
        BasketballShot layup = new BasketballShot(2.0, 2.5, 1.5, 2.9);
        v0 = 6.0;
        // 打板投篮通常需要更高角度
        optAngle = layup.findOptimalAngle(v0, 60.0, 0.3, 1000);
        System.out.println("  固定初速 " + v0 + "m/s,梯度下降最优角度: "
                         + String.format("%.4f", optAngle) + "°");
        System.out.println("  " + layup.judgeShot(v0, optAngle));

        // 场景4:不同距离的最低出手速度
        System.out.println("\n【场景4】不同距离下的最低出手速度(角度45°)");
        for (double d = 2.0; d <= 8.0; d += 1.0) {
            BasketballShot shot = new BasketballShot(2.0, d, -1, 0);
            double minV0 = 6.0;
            while (shot.analyticalAngles(minV0).length == 0 && minV0 < 20.0) {
                minV0 += 0.1;
            }
            System.out.println("  距离 " + d + "m, 最低出手速度: "
                             + String.format("%.2f", minV0) + "m/s");
        }
    }
}

四、复杂度分析

模块 时间复杂度 空间复杂度 说明
轨迹模拟 (O(T/dt)) (O(T/dt)) (T)为飞行时间,(dt)为积分步长
损失计算 (O(1)) (O(1)) 直接代入解析公式
梯度下降 (O(k)) (O(1)) (k)为迭代次数,通常 (k < 100)
解析求解 (O(1)) (O(1)) 直接开方计算

梯度下降的收敛速度取决于学习率的选取。对于投篮损失函数(关于角度的单峰凸函数),适当的学习率可在数十次迭代内收敛到机器精度。

五、扩展思考

  1. 空气阻力模型:高速投篮时空气阻力不可忽略,可在竖直和水平方向分别加入与速度平方成正比的阻力项,使用Runge-Kutta方法进行更精确的数值积分。
  2. 风速补偿:引入水平风速变量,将风速叠加到水平速度上,重新求解最优角度。
  3. 多角度投篮机器人:将本系统封装为投篮机器人的决策模块,根据实时测量的距离和高度,动态计算最优出手参数。

投篮问题将经典物理模型与数值优化算法紧密结合,是理解梯度下降在实际场景中应用的绝佳案例。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注