游戏背景与问题建模
丢沙包是中国传统的投掷游戏,玩家需要将沙包投入指定目标区域(如方格、圆圈或桶中)以获得分数。投掷的准确性受出手角度、力度、风向等多种因素影响。本文将其抽象为抛物线运动与概率模拟问题:给定投掷起点与目标位置,计算最优出手角度与速度;同时通过蒙特卡洛模拟估计不同策略下的命中率。
算法思路
抛物线运动建模
忽略空气阻力,沙包的运动轨迹满足抛物线方程。设出手速度为 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),通过大量随机采样模拟投掷结果,统计命中率:
- 设定目标区域范围
- 对每次模拟,在理论最优值附近加入随机扰动
- 计算落点位置
- 统计落入目标区域的次数占比
风向补偿
引入恒定水平风力 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)。
扩展思考
- 三维场景:目标为三维空间中的点或区域,引入方位角和俯仰角两个自由度。
- 空气阻力:高速投掷需考虑与速度平方成正比的阻力,使用龙格-库塔法数值求解微分方程。
- 机器学习校准:通过多次投掷数据训练回归模型,自动修正系统误差和个体偏差。
- 多目标优化:同时优化命中率和能量消耗(出手力度),使用帕累托前沿分析权衡方案。