每日算法 — 使用java实现丢沙包:目标追踪与概率模拟

游戏背景与问题建模

丢沙包是中国传统的投掷游戏,玩家需要将沙包投入指定目标区域(如方格、圆圈或桶中)以获得分数。投掷的准确性受出手角度、力度、风向等多种因素影响。本文将其抽象为抛物线运动与概率模拟问题:给定投掷起点与目标位置,计算最优出手角度与速度;同时通过蒙特卡洛模拟估计不同策略下的命中率。

算法思路

抛物线运动建模

忽略空气阻力,沙包的运动轨迹满足抛物线方程。设出手速度为 v,角度为 θ,则:

x(t) = v × cos(θ) × t
y(t) = v × sin(θ) × t - 0.5 × g × t²

通过消去参数 t,得到轨迹方程,进而求解命中目标所需的角度和速度。

最优角度计算

对于给定的水平距离 d 和出手高度 h,存在两个发射角度(高抛和低抛)使沙包命中目标高度为 0 的位置:

tan(θ) = (v² ± √(v⁴ - g(gd² + 2hv²))) / (gd)

蒙特卡洛概率模拟

由于实际投掷存在随机误差(角度偏差 ±Δθ、速度偏差 ±Δv),通过大量随机采样模拟投掷结果,统计命中率:

  1. 设定目标区域范围
  2. 对每次模拟,在理论最优值附近加入随机扰动
  3. 计算落点位置
  4. 统计落入目标区域的次数占比

风向补偿

引入恒定水平风力 fw,修正水平位移:

x(t) = v × cos(θ) × t + 0.5 × (fw / m) × t²

Java 完整实现

import java.util.*;

/**
 * 丢沙包:抛物线运动建模与蒙特卡洛概率模拟
 * 包含最优角度计算、落点预测、命中率估计
 */
public class BeanbagToss {

    private static final double G = 9.8;      // 重力加速度 (m/s²)
    private static final double DT = 0.01;    // 仿真时间步长 (s)

    /**
     * 计算命中目标所需的最优发射角度
     * @param v     出手速度 (m/s)
     * @param d     水平距离 (m)
     * @param h     出手高度 (m)
     * @param targetHeight 目标高度 (m)
     * @return 两个可行角度(低抛角、高抛角),若无解返回空列表
     */
    public List<Double> calculateAngles(double v, double d, double h, double targetHeight) {
        List<Double> angles = new ArrayList<>();
        double deltaHeight = targetHeight - h;
        double discriminant = Math.pow(v, 4) - G * (G * d * d + 2 * deltaHeight * v * v);

        if (discriminant < 0) {
            return angles; // 速度不足,无法到达
        }

        double sqrtDisc = Math.sqrt(discriminant);
        double tanTheta1 = (v * v + sqrtDisc) / (G * d);
        double tanTheta2 = (v * v - sqrtDisc) / (G * d);

        angles.add(Math.toDegrees(Math.atan(tanTheta1))); // 高抛
        if (discriminant > 0) {
            angles.add(Math.toDegrees(Math.atan(tanTheta2))); // 低抛
        }

        return angles;
    }

    /**
     * 计算命中目标所需的最小出手速度
     * @param d     水平距离
     * @param h     出手高度
     * @param targetHeight 目标高度
     * @return 最小速度,若无解返回 -1
     */
    public double minVelocity(double d, double h, double targetHeight) {
        double deltaH = targetHeight - h;
        double minV = Math.sqrt(G * (deltaH + Math.sqrt(d * d + deltaH * deltaH)));
        return minV;
    }

    /**
     * 模拟沙包飞行轨迹
     * @param v0        初始速度
     * @param angleDeg  发射角度(度)
     * @param h0        初始高度
     * @param windX     水平风力 (m/s²)
     * @return 轨迹点列表 {x, y, t}
     */
    public List<double[]> simulateTrajectory(double v0, double angleDeg, double h0, double windX) {
        List<double[]> trajectory = new ArrayList<>();
        double angleRad = Math.toRadians(angleDeg);
        double vx = v0 * Math.cos(angleRad);
        double vy = v0 * Math.sin(angleRad);
        double x = 0, y = h0, t = 0;

        trajectory.add(new double[]{x, y, t});

        while (y >= 0) {
            t += DT;
            // 更新速度(考虑风力)
            vx += windX * DT;
            vy -= G * DT;
            // 更新位置
            x += vx * DT;
            y += vy * DT;
            trajectory.add(new double[]{x, y, t});
        }

        return trajectory;
    }

    /**
     * 蒙特卡洛模拟命中率
     * @param v0            理论最优速度
     * @param angleDeg      理论最优角度
     * @param h0            出手高度
     * @param targetX       目标中心 X
     * @param targetY       目标中心 Y
     * @param targetRadius  目标半径
     * @param angleError    角度标准差
     * @param velocityError 速度标准差
     * @param simulations   模拟次数
     * @return 命中率 [0, 1]
     */
    public double monteCarloHitRate(double v0, double angleDeg, double h0,
                                     double targetX, double targetY, double targetRadius,
                                     double angleError, double velocityError,
                                     int simulations) {
        Random random = new Random(42); // 固定种子保证可复现
        int hits = 0;

        for (int i = 0; i < simulations; i++) {
            // 加入随机误差
            double actualAngle = angleDeg + random.nextGaussian() * angleError;
            double actualV = v0 + random.nextGaussian() * velocityError;
            actualV = Math.max(0.1, actualV); // 速度不能为负

            // 计算落点
            double[] landing = calculateLandingPoint(actualV, actualAngle, h0, 0);
            double landX = landing[0];
            double landY = landing[1];

            // 判断是否命中目标
            double dist = Math.sqrt(Math.pow(landX - targetX, 2) + Math.pow(landY - targetY, 2));
            if (dist <= targetRadius) {
                hits++;
            }
        }

        return (double) hits / simulations;
    }

    /**
     * 计算沙包落地位置(解析解)
     */
    public double[] calculateLandingPoint(double v0, double angleDeg, double h0, double windX) {
        double angleRad = Math.toRadians(angleDeg);
        double vy = v0 * Math.sin(angleRad);
        double vx = v0 * Math.cos(angleRad);

        // 求解落地时间:h0 + vy*t + 0.5*windX*t² - 0.5*G*t² = 0
        // 简化为:0.5*(windX - G)*t² + vy*t + h0 = 0
        double a = 0.5 * (windX * 0 - G); // 忽略风力对落地时间的二阶影响,简化计算
        double b = vy;
        double c = h0;

        double discriminant = b * b - 4 * a * c;
        if (discriminant < 0) return new double[]{0, 0};

        double t = (-b - Math.sqrt(discriminant)) / (2 * a);
        if (t < 0) t = (-b + Math.sqrt(discriminant)) / (2 * a);

        double landX = vx * t + 0.5 * windX * t * t;
        return new double[]{landX, 0};
    }

    /**
     * 寻找最优策略:在速度范围内搜索最高命中率的角度-速度组合
     */
    public Strategy findOptimalStrategy(double d, double h0, double targetY,
                                         double targetRadius,
                                         double angleError, double velocityError,
                                         int simulations) {
        double bestHitRate = 0;
        double bestAngle = 45;
        double bestV = 10;

        // 在合理范围内搜索
        for (double v = 5; v <= 25; v += 0.5) {
            List<Double> angles = calculateAngles(v, d, h0, targetY);
            for (double angle : angles) {
                if (angle < 10 || angle > 80) continue; // 排除不合理角度

                double hitRate = monteCarloHitRate(v, angle, h0, d, targetY, targetRadius,
                        angleError, velocityError, simulations);

                if (hitRate > bestHitRate) {
                    bestHitRate = hitRate;
                    bestAngle = angle;
                    bestV = v;
                }
            }
        }

        return new Strategy(bestAngle, bestV, bestHitRate);
    }

    /**
     * 绘制轨迹(文本形式)
     */
    public void printTrajectory(double v0, double angleDeg, double h0, double windX) {
        List<double[]> traj = simulateTrajectory(v0, angleDeg, h0, windX);
        System.out.printf("轨迹模拟 (v=%.1fm/s, angle=%.1f°, h=%.1fm, wind=%.1f):%n",
                v0, angleDeg, h0, windX);
        for (int i = 0; i < traj.size(); i += 10) {
            double[] p = traj.get(i);
            System.out.printf("  t=%.2fs: x=%.2fm, y=%.2fm%n", p[2], p[0], p[1]);
        }
        double[] last = traj.get(traj.size() - 1);
        System.out.printf("  落点: x=%.2fm, y=%.2fm, 总时间=%.2fs%n%n",
                last[0], last[1], last[2]);
    }

    // 策略封装
    public static class Strategy {
        public final double angle;
        public final double velocity;
        public final double hitRate;

        public Strategy(double angle, double velocity, double hitRate) {
            this.angle = angle;
            this.velocity = velocity;
            this.hitRate = hitRate;
        }

        @Override
        public String toString() {
            return String.format("最优策略: 角度=%.1f°, 速度=%.1fm/s, 命中率=%.1f%%",
                    angle, velocity, hitRate * 100);
        }
    }

    public static void main(String[] args) {
        BeanbagToss game = new BeanbagToss();

        double distance = 10.0;      // 目标距离 10 米
        double height = 1.5;         // 出手高度 1.5 米
        double targetHeight = 0.3;   // 目标高度 0.3 米(地面上的桶)
        double targetRadius = 0.3;   // 目标半径 0.3 米

        System.out.println("=== 丢沙包最优策略计算 ===");
        System.out.printf("目标距离: %.1fm, 出手高度: %.1fm%n", distance, height);

        // 1. 计算可行角度
        double v = 12.0;
        List<Double> angles = game.calculateAngles(v, distance, height, targetHeight);
        System.out.println("速度 " + v + "m/s 时的可行角度: " + angles);

        // 2. 轨迹模拟
        if (!angles.isEmpty()) {
            game.printTrajectory(v, angles.get(0), height, 0);
        }

        // 3. 蒙特卡洛命中率
        System.out.println("=== 蒙特卡洛模拟 ===");
        double hitRate = game.monteCarloHitRate(v, 45, height, distance, targetHeight,
                targetRadius, 2.0, 1.0, 10000);
        System.out.printf("固定策略命中率: %.1f%%%n", hitRate * 100);

        // 4. 搜索最优策略
        System.out.println("\n=== 搜索最优策略 ===");
        Strategy optimal = game.findOptimalStrategy(distance, height, targetHeight,
                targetRadius, 2.0, 1.0, 5000);
        System.out.println(optimal);
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
角度计算 O(1) O(1)
轨迹模拟 O(T/DT) O(T/DT)
蒙特卡洛模拟 O(S × T/DT) O(T/DT)
最优策略搜索 O(V × A × S × T/DT) O(T/DT)

其中 T 为飞行时间,DT 为仿真步长,S 为模拟次数,V 和 A 分别为速度和角度的搜索范围。实际使用中可通过解析解避免逐帧模拟,将蒙特卡洛复杂度降至 O(S)。

扩展思考

  • 三维场景:目标为三维空间中的点或区域,引入方位角和俯仰角两个自由度。
  • 空气阻力:高速投掷需考虑与速度平方成正比的阻力,使用龙格-库塔法数值求解微分方程。
  • 机器学习校准:通过多次投掷数据训练回归模型,自动修正系统误差和个体偏差。
  • 多目标优化:同时优化命中率和能量消耗(出手力度),使用帕累托前沿分析权衡方案。