每日算法 — 使用java实现射箭:物理抛物线模拟与梯度下降最优角度

引言

射箭是一项融合力量控制、角度计算与风速补偿的精密运动。从算法角度看,射中目标的核心在于求解最优发射角度——这涉及经典力学中的抛物线运动建模,以及数值优化中的梯度下降方法。本文将使用Java实现射箭最优角度计算系统,详细讲解抛物线物理运动学建模、梯度下降算法迭代求解最优发射角度,以及风阻补偿与距离预测机制。

物理模型建立

假设箭矢在二维平面内运动,忽略箭矢自转和升力,考虑重力和空气阻力:

运动方程:
  dx/dt = vx
  dy/dt = vy
  dvx/dt = -k * vx * |v|  (空气阻力,与速度平方成正比)
  dvy/dt = -g - k * vy * |v|

其中k为空气阻力系数,g为重力加速度(9.8 m/s²)。

public class ProjectilePhysics {
    public static final double G = 9.8; // 重力加速度 (m/s^2)
    private double dragCoefficient; // 空气阻力系数
    private double mass; // 箭矢质量 (kg)
    private double crossSectionalArea; // 截面积 (m^2)
    private double airDensity = 1.225; // 空气密度 (kg/m^3)

    public ProjectilePhysics(double mass, double diameter) {
        this.mass = mass;
        this.crossSectionalArea = Math.PI * Math.pow(diameter / 2, 2);
        // 阻力系数 C_d ≈ 0.5 对于流线型物体
        this.dragCoefficient = 0.5 * airDensity * crossSectionalArea / mass;
    }

    /**
     * 使用四阶Runge-Kutta方法数值求解运动方程
     */
    public Trajectory simulateTrajectory(double v0, double angle, 
                                          double windX, double windY,
                                          double timeStep, double maxTime) {
        List<Double> xList = new ArrayList<>();
        List<Double> yList = new ArrayList<>();
        List<Double> tList = new ArrayList<>();

        double x = 0, y = 0;
        double vx = v0 * Math.cos(angle);
        double vy = v0 * Math.sin(angle);
        double t = 0;

        xList.add(x); yList.add(y); tList.add(t);

        while (t < maxTime && y >= 0) {
            // 相对风速
            double relVx = vx - windX;
            double relVy = vy - windY;
            double vRel = Math.sqrt(relVx * relVx + relVy * relVy);

            // RK4步进
            State s = new State(x, y, vx, vy);
            State k1 = derivative(s, windX, windY);
            State k2 = derivative(add(s, multiply(k1, 0.5 * timeStep)), windX, windY);
            State k3 = derivative(add(s, multiply(k2, 0.5 * timeStep)), windX, windY);
            State k4 = derivative(add(s, multiply(k3, timeStep)), windX, windY);

            State delta = multiply(add(add(k1, multiply(k2, 2)), 
                                       add(multiply(k3, 2), k4)), timeStep / 6.0);

            x += delta.x;
            y += delta.y;
            vx += delta.vx;
            vy += delta.vy;
            t += timeStep;

            xList.add(x); yList.add(y); tList.add(t);
        }

        return new Trajectory(xList, yList, tList);
    }

    private State derivative(State s, double windX, double windY) {
        double relVx = s.vx - windX;
        double relVy = s.vy - windY;
        double vRel = Math.sqrt(relVx * relVx + relVy * relVy);

        double dragX = -dragCoefficient * vRel * relVx;
        double dragY = -dragCoefficient * vRel * relVy;

        return new State(s.vx, s.vy, dragX, dragY - G);
    }

    // 简化状态类
    static class State {
        double x, y, vx, vy;
        State(double x, double y, double vx, double vy) {
            this.x = x; this.y = y; this.vx = vx; this.vy = vy;
        }
    }

    private State add(State a, State b) {
        return new State(a.x + b.x, a.y + b.y, a.vx + b.vx, a.vy + b.vy);
    }

    private State multiply(State s, double scalar) {
        return new State(s.x * scalar, s.y * scalar, s.vx * scalar, s.vy * scalar);
    }

    public static class Trajectory {
        public final List<Double> x, y, t;
        Trajectory(List<Double> x, List<Double> y, List<Double> t) {
            this.x = x; this.y = y; this.t = t;
        }

        /**
         * 查找落点(y=targetHeight时的x坐标)
         */
        public double findLandingX(double targetHeight) {
            for (int i = 1; i < y.size(); i++) {
                if (y.get(i) <= targetHeight && y.get(i-1) > targetHeight) {
                    // 线性插值
                    double ratio = (targetHeight - y.get(i-1)) / (y.get(i) - y.get(i-1));
                    return x.get(i-1) + ratio * (x.get(i) - x.get(i-1));
                }
            }
            return x.get(x.size() - 1);
        }

        /**
         * 查找轨迹最高点
         */
        public double findMaxHeight() {
            return y.stream().mapToDouble(Double::doubleValue).max().orElse(0);
        }
    }
}

梯度下降求解最优角度

public class OptimalAngleSolver {
    private final ProjectilePhysics physics;
    private static final double EPSILON = 1e-6;
    private static final double LEARNING_RATE = 0.01;
    private static final int MAX_ITERATIONS = 1000;

    public OptimalAngleSolver(ProjectilePhysics physics) {
        this.physics = physics;
    }

    /**
     * 使用梯度下降求解命中目标的最优发射角度
     * 损失函数:L = (落点x - 目标距离)^2 + (轨迹最高点 - 目标高度)^2(可选)
     */
    public double solveOptimalAngle(double targetDistance, double targetHeight,
                                     double v0, double windX, double windY) {
        double angle = Math.toRadians(45); // 初始猜测:45度
        double bestAngle = angle;
        double bestLoss = Double.POSITIVE_INFINITY;

        for (int iter = 0; iter < MAX_ITERATIONS; iter++) {
            // 计算当前角度下的落点
            ProjectilePhysics.Trajectory traj = physics.simulateTrajectory(
                v0, angle, windX, windY, 0.01, 10.0);
            double landingX = traj.findLandingX(targetHeight);

            double loss = Math.pow(landingX - targetDistance, 2);

            if (loss < bestLoss) {
                bestLoss = loss;
                bestAngle = angle;
            }

            if (loss < EPSILON) break; // 收敛

            // 数值计算梯度
            double delta = 1e-4;
            ProjectilePhysics.Trajectory trajPlus = physics.simulateTrajectory(
                v0, angle + delta, windX, windY, 0.01, 10.0);
            double landingPlus = trajPlus.findLandingX(targetHeight);
            double lossPlus = Math.pow(landingPlus - targetDistance, 2);

            double gradient = (lossPlus - loss) / delta;

            // 梯度下降更新
            angle -= LEARNING_RATE * gradient;

            // 限制角度在合理范围
            angle = Math.max(Math.toRadians(5), Math.min(Math.toRadians(85), angle));
        }

        return bestAngle;
    }

    /**
     * 牛顿法加速收敛:利用二阶导数信息
     */
    public double solveWithNewtonMethod(double targetDistance, double targetHeight,
                                         double v0, double windX, double windY) {
        double angle = Math.toRadians(45);
        double timeStep = 0.01;

        for (int iter = 0; iter < 50; iter++) {
            ProjectilePhysics.Trajectory traj = physics.simulateTrajectory(
                v0, angle, windX, windY, timeStep, 10.0);
            double f = traj.findLandingX(targetHeight) - targetDistance;

            if (Math.abs(f) < EPSILON) return angle;

            // 数值求导 f'(angle)
            double delta = 1e-5;
            ProjectilePhysics.Trajectory trajD = physics.simulateTrajectory(
                v0, angle + delta, windX, windY, timeStep, 10.0);
            double fp = (trajD.findLandingX(targetHeight) - traj.findLandingX(targetHeight)) / delta;

            if (Math.abs(fp) < EPSILON) break;

            angle -= f / fp; // 牛顿迭代
            angle = Math.max(Math.toRadians(5), Math.min(Math.toRadians(85), angle));
        }

        return angle;
    }

    /**
     * 二分搜索求解(适用于无风阻的理想抛物线)
     */
    public double solveWithBinarySearch(double targetDistance, double targetHeight, double v0) {
        double low = Math.toRadians(5);
        double high = Math.toRadians(85);

        for (int iter = 0; iter < 50; iter++) {
            double mid = (low + high) / 2;

            // 理想抛物线公式(无风阻)
            double range = (v0 * v0 * Math.sin(2 * mid)) / ProjectilePhysics.G;

            if (range < targetDistance) {
                low = mid;
            } else {
                high = mid;
            }

            if (high - low < EPSILON) break;
        }

        return (low + high) / 2;
    }
}

风速补偿与自适应调整

public class WindCompensator {
    private final ProjectilePhysics physics;
    private final OptimalAngleSolver solver;

    public WindCompensator(ProjectilePhysics physics) {
        this.physics = physics;
        this.solver = new OptimalAngleSolver(physics);
    }

    /**
     * 计算风向补偿量
     * 侧风主要影响落点的横向偏移,需要调整瞄准点
     */
    public double calculateWindageCompensation(double targetDistance, double v0,
                                                double windSpeed, double windAngle) {
        // 分解风速
        double windX = windSpeed * Math.cos(windAngle); // 顺风/逆风分量
        double windY = windSpeed * Math.sin(windAngle); // 侧风分量(简化处理)

        // 无风时的基准角度
        double baseAngle = solver.solveWithBinarySearch(targetDistance, 0, v0);

        // 有风时的最优角度
        double adjustedAngle = solver.solveOptimalAngle(targetDistance, 0, v0, windX, windY);

        return adjustedAngle - baseAngle; // 角度补偿量
    }

    /**
     * 风向预测:基于历史风速数据的移动平均
     */
    public double predictWindSpeed(List<Double> windHistory, int windowSize) {
        if (windHistory.isEmpty()) return 0;

        int n = Math.min(windowSize, windHistory.size());
        double sum = 0;
        for (int i = windHistory.size() - n; i < windHistory.size(); i++) {
            sum += windHistory.get(i);
        }
        return sum / n;
    }

    /**
     * 综合射击方案:考虑距离、高度差、风速
     */
    public ShootingSolution calculateSolution(double targetDistance, double targetHeight,
                                               double v0, double windSpeed, double windAngle) {
        double windX = windSpeed * Math.cos(windAngle);
        double windY = windSpeed * Math.sin(windAngle);

        double optimalAngle = solver.solveOptimalAngle(targetDistance, targetHeight, v0, windX, windY);

        // 模拟验证
        ProjectilePhysics.Trajectory traj = physics.simulateTrajectory(
            v0, optimalAngle, windX, windY, 0.01, 10.0);
        double actualDistance = traj.findLandingX(targetHeight);
        double maxHeight = traj.findMaxHeight();
        double flightTime = traj.t.get(traj.t.size() - 1);

        return new ShootingSolution(optimalAngle, actualDistance, maxHeight, flightTime);
    }

    public static class ShootingSolution {
        public final double angle; // 弧度
        public final double predictedDistance; // 预测落点
        public final double maxHeight; // 轨迹最高点
        public final double flightTime; // 飞行时间

        public ShootingSolution(double angle, double dist, double height, double time) {
            this.angle = angle;
            this.predictedDistance = dist;
            this.maxHeight = height;
            this.flightTime = time;
        }

        @Override
        public String toString() {
            return String.format("发射角度: %.2f°, 预测落点: %.2fm, 最高点: %.2fm, 飞行时间: %.2fs",
                Math.toDegrees(angle), predictedDistance, maxHeight, flightTime);
        }
    }
}

完整模拟系统

public class ArcherySimulator {
    public static void main(String[] args) {
        // 箭矢参数:质量30g,直径8mm
        ProjectilePhysics physics = new ProjectilePhysics(0.030, 0.008);
        OptimalAngleSolver solver = new OptimalAngleSolver(physics);
        WindCompensator compensator = new WindCompensator(physics);

        double v0 = 70.0; // 初速度 70 m/s
        double targetDistance = 50.0; // 目标距离 50米
        double targetHeight = 0.0; // 目标高度

        System.out.println("=== 射箭最优角度计算 ===");
        System.out.printf("箭矢初速度: %.1f m/s%n", v0);
        System.out.printf("目标距离: %.1f m%n", targetDistance);

        // 场景1:无风
        System.out.println("\n--- 无风条件 ---");
        double idealAngle = solver.solveOptimalAngle(targetDistance, targetHeight, v0, 0, 0);
        System.out.printf("最优发射角度: %.2f°%n", Math.toDegrees(idealAngle));

        ProjectilePhysics.Trajectory idealTraj = physics.simulateTrajectory(
            v0, idealAngle, 0, 0, 0.01, 10.0);
        System.out.printf("实际落点: %.2f m%n", idealTraj.findLandingX(targetHeight));
        System.out.printf("轨迹最高点: %.2f m%n", idealTraj.findMaxHeight());

        // 场景2:顺风 5m/s
        System.out.println("\n--- 顺风 5m/s ---");
        double tailwindAngle = solver.solveOptimalAngle(targetDistance, targetHeight, v0, 5.0, 0);
        System.out.printf("补偿后角度: %.2f° (调整 %.2f°)%n",
            Math.toDegrees(tailwindAngle), Math.toDegrees(tailwindAngle - idealAngle));

        // 场景3:逆风 3m/s
        System.out.println("\n--- 逆风 3m/s ---");
        double headwindAngle = solver.solveOptimalAngle(targetDistance, targetHeight, v0, -3.0, 0);
        System.out.printf("补偿后角度: %.2f° (调整 %.2f°)%n",
            Math.toDegrees(headwindAngle), Math.toDegrees(headwindAngle - idealAngle));

        // 场景4:综合射击方案
        System.out.println("\n--- 综合射击方案 ---");
        WindCompensator.ShootingSolution solution = compensator.calculateSolution(
            targetDistance, targetHeight, v0, 2.0, Math.toRadians(30));
        System.out.println(solution);

        // 对比不同算法的收敛速度
        System.out.println("\n=== 算法性能对比 ===");
        long start1 = System.nanoTime();
        solver.solveOptimalAngle(targetDistance, targetHeight, v0, 0, 0);
        long gdTime = System.nanoTime() - start1;

        long start2 = System.nanoTime();
        solver.solveWithNewtonMethod(targetDistance, targetHeight, v0, 0, 0);
        long newtonTime = System.nanoTime() - start2;

        long start3 = System.nanoTime();
        solver.solveWithBinarySearch(targetDistance, targetHeight, v0);
        long bsTime = System.nanoTime() - start3;

        System.out.printf("梯度下降: %.3f ms%n", gdTime / 1e6);
        System.out.printf("牛顿法: %.3f ms%n", newtonTime / 1e6);
        System.out.printf("二分搜索(理想): %.3f ms%n", bsTime / 1e6);
    }
}

复杂度分析

模块 时间复杂度 空间复杂度
RK4轨迹模拟(单步) O(1) O(1)
完整轨迹模拟(T秒) O(T/Δt) O(T/Δt)
梯度下降求解 O(iter × T/Δt) O(T/Δt)
牛顿法求解 O(iter × T/Δt) O(T/Δt)
二分搜索(理想情况) O(iter) O(1)

其中Δt为时间步长,iter为迭代次数,T为最大模拟时间。

总结

本文从经典力学和数值优化两个维度分析了射箭问题,通过Runge-Kutta方法精确模拟了带空气阻力的抛物线运动,并运用梯度下降和牛顿法求解最优发射角度。风阻补偿模块进一步提升了系统的实用性。这些算法思想可以推广到弹道计算、航天器轨道设计等更广泛的工程领域。