每日算法 — 使用java实现飞镖游戏:抛物线轨迹与概率最优靶区选择

引言

飞镖是一项经典的投掷运动,靶面由多个同心圆环和扇形区域组成,不同区域对应不同分值。对于算法研究者而言,飞镖游戏蕴含丰富的数学问题:给定投掷者的精准度分布,如何选择瞄准点使期望得分最大化?

本文用Java实现飞镖得分计算与最优靶区选择系统,涵盖几何坐标建模、二维概率密度积分、蒙特卡洛模拟与贪心最优策略。

问题建模与算法思路

飞镖靶结构

标准飞镖靶(Dartboard)由以下区域组成:
– 20个扇形编号区(1-20分),每个扇形占18度。
– 单倍区(Single):基础分值。
– 双倍环(Double Ring,半径约170mm):分值 $\times$ 2。
– 三倍环(Triple Ring,半径约107mm):分值 $\times$ 3。
– 外牛眼(Outer Bull,25分)与内牛眼(Inner Bull,50分)。

核心算法思路

  1. 几何坐标建模:将靶面映射到二维坐标系,每个区域由极坐标条件定义。
  2. 精准度建模:假设投掷落点服从以瞄准点为中心的二维正态分布 $N(\mu, \sigma^2)$。
  3. 期望得分计算:对目标区域进行概率积分或蒙特卡洛采样,计算瞄准某点的期望得分。
  4. 最优靶区搜索:在靶面上采样候选点,通过比较期望得分找到全局最优瞄准点。

完整Java实现

import java.util.*;

/**
 * 飞镖游戏:抛物线轨迹与概率最优靶区选择
 * 核心算法:几何坐标建模 + 二维正态分布概率积分 + 蒙特卡洛最优搜索
 */
public class DartGameSolver {

    // 飞镖靶标准尺寸参数(单位:毫米)
    static final double INNER_BULL_RADIUS = 6.35;
    static final double OUTER_BULL_RADIUS = 15.9;
    static final double INNER_TRIPLE_RADIUS = 99.0;
    static final double OUTER_TRIPLE_RADIUS = 107.0;
    static final double INNER_DOUBLE_RADIUS = 162.0;
    static final double OUTER_DOUBLE_RADIUS = 170.0;
    static final double BOARD_RADIUS = 225.0;

    // 20个扇形区域的编号顺序(从顶部顺时针)
    static final int[] SECTOR_ORDER = {20, 1, 18, 4, 13, 6, 10, 15, 2, 17,
                                        3, 19, 7, 16, 8, 11, 14, 9, 12, 5};

    // 投掷精准度参数(标准差,单位:毫米)
    private double sigma;
    private Random random;

    public DartGameSolver(double sigma) {
        this.sigma = sigma;
        this.random = new Random(42);
    }

    /**
     * 将极坐标(r, theta)转换为分值
     * theta: 弧度,从x轴正方向逆时针计算
     */
    public int scoreAt(double r, double theta) {
        // 规范化角度到 [0, 2π)
        double normTheta = ((theta % (2 * Math.PI)) + (2 * Math.PI)) % (2 * Math.PI);
        // 飞镖靶扇形编号从顶部(π/2)开始顺时针
        // 将我们的角度转换为靶面角度:从顶部顺时针偏移
        double boardAngle = (Math.PI / 2 - normTheta + 2 * Math.PI) % (2 * Math.PI);
        int sectorIndex = (int) Math.floor(boardAngle / (2 * Math.PI / 20));
        sectorIndex = Math.min(sectorIndex, 19);
        int baseScore = SECTOR_ORDER[sectorIndex];

        if (r <= INNER_BULL_RADIUS) return 50; // 内牛眼
        if (r <= OUTER_BULL_RADIUS) return 25; // 外牛眼
        if (r > BOARD_RADIUS) return 0;        // 脱靶

        boolean isDouble = (r >= INNER_DOUBLE_RADIUS && r <= OUTER_DOUBLE_RADIUS);
        boolean isTriple = (r >= INNER_TRIPLE_RADIUS && r <= OUTER_TRIPLE_RADIUS);

        if (isDouble) return baseScore * 2;
        if (isTriple) return baseScore * 3;
        return baseScore; // 单倍区
    }

    /**
     * 计算给定笛卡尔坐标(x,y)的分值
     */
    public int scoreAtXY(double x, double y) {
        double r = Math.sqrt(x * x + y * y);
        double theta = Math.atan2(y, x);
        return scoreAt(r, theta);
    }

    /**
     * 蒙特卡洛模拟:计算瞄准点(aimX, aimY)的期望得分
     * 假设落点服从二维正态分布 N((aimX,aimY), sigma^2)
     */
    public double expectedScoreMonteCarlo(double aimX, double aimY, int samples) {
        double totalScore = 0;
        for (int i = 0; i < samples; i++) {
            double dx = random.nextGaussian() * sigma;
            double dy = random.nextGaussian() * sigma;
            totalScore += scoreAtXY(aimX + dx, aimY + dy);
        }
        return totalScore / samples;
    }

    /**
     * 解析法近似:利用二维正态分布的概率密度,对靶面区域进行网格积分
     * 计算瞄准点(aimX, aimY)的期望得分
     */
    public double expectedScoreGridIntegration(double aimX, double aimY, double gridSize) {
        double totalScore = 0;
        double totalWeight = 0;
        int steps = (int) (BOARD_RADIUS * 2 / gridSize);

        for (int i = 0; i <= steps; i++) {
            double x = -BOARD_RADIUS + i * gridSize;
            for (int j = 0; j <= steps; j++) {
                double y = -BOARD_RADIUS + j * gridSize;
                double r = Math.sqrt(x * x + y * y);
                if (r > BOARD_RADIUS + gridSize) continue;

                // 二维正态分布概率密度在(x,y)处的值
                double dx = x - aimX;
                double dy = y - aimY;
                double pdf = Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma))
                        / (2 * Math.PI * sigma * sigma);

                int score = scoreAtXY(x, y);
                totalScore += score * pdf * gridSize * gridSize;
                totalWeight += pdf * gridSize * gridSize;
            }
        }
        return totalScore; // 由于网格有限,总权重可能略小于1,但不影响相对比较
    }

    /**
     * 贪心最优靶区搜索:在靶面上采样候选瞄准点,返回期望得分最高的点
     */
    public double[] findOptimalAimPoint(int samples, double searchRadius) {
        double bestX = 0, bestY = 0;
        double bestScore = -1;

        // 极坐标均匀采样候选点
        for (int i = 0; i < samples; i++) {
            double r = Math.sqrt(random.nextDouble()) * searchRadius;
            double theta = random.nextDouble() * 2 * Math.PI;
            double x = r * Math.cos(theta);
            double y = r * Math.sin(theta);

            double expScore = expectedScoreMonteCarlo(x, y, 500);
            if (expScore > bestScore) {
                bestScore = expScore;
                bestX = x;
                bestY = y;
            }
        }
        return new double[]{bestX, bestY, bestScore};
    }

    /**
     * 局部精细搜索:在最优点附近进行梯度上升式搜索
     */
    public double[] refineOptimalAimPoint(double startX, double startY, double stepSize, int iterations) {
        double cx = startX, cy = startY;
        double currentScore = expectedScoreMonteCarlo(cx, cy, 2000);

        for (int iter = 0; iter < iterations; iter++) {
            boolean improved = false;
            // 8邻域搜索
            double[][] dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}, {1,1}, {1,-1}, {-1,1}, {-1,-1}};
            for (double[] d : dirs) {
                double nx = cx + d[0] * stepSize;
                double ny = cy + d[1] * stepSize;
                double score = expectedScoreMonteCarlo(nx, ny, 2000);
                if (score > currentScore) {
                    currentScore = score;
                    cx = nx;
                    cy = ny;
                    improved = true;
                }
            }
            if (!improved) {
                stepSize *= 0.5; // 缩小步长
                if (stepSize < 0.1) break;
            }
        }
        return new double[]{cx, cy, currentScore};
    }

    /**
     * 可视化输出靶面得分热力图
     */
    public void printHeatMap(int resolution) {
        double range = BOARD_RADIUS;
        double step = 2 * range / resolution;
        System.out.println("飞镖靶期望得分热力图(sigma=" + sigma + ")");
        for (int row = 0; row < resolution; row++) {
            double y = range - row * step;
            StringBuilder line = new StringBuilder();
            for (int col = 0; col < resolution; col++) {
                double x = -range + col * step;
                double score = expectedScoreGridIntegration(x, y, step);
                // 用字符表示得分等级
                if (score > 45) line.append("@");
                else if (score > 35) line.append("#");
                else if (score > 25) line.append("*");
                else if (score > 15) line.append("+");
                else if (score > 5)  line.append("-");
                else if (score > 1)  line.append(".");
                else line.append(" ");
            }
            System.out.println(line);
        }
    }

    public static void main(String[] args) {
        // 创建一个精准度sigma=30mm的玩家
        DartGameSolver solver = new DartGameSolver(30.0);

        System.out.println("=== 飞镖最优靶区选择 ===");
        System.out.println("玩家精准度 sigma = 30mm");

        // 验证几个固定瞄准点的期望得分
        System.out.println("\n瞄准中心(0,0)的期望得分: " +
            String.format("%.2f", solver.expectedScoreMonteCarlo(0, 0, 10000)));
        System.out.println("瞄准三倍20区中心(0,96)的期望得分: " +
            String.format("%.2f", solver.expectedScoreMonteCarlo(0, 96, 10000)));
        System.out.println("瞄准内牛眼上方(0,10)的期望得分: " +
            String.format("%.2f", solver.expectedScoreMonteCarlo(0, 10, 10000)));

        // 全局最优搜索
        System.out.println("\n正在搜索全局最优瞄准点...");
        double[] coarse = solver.findOptimalAimPoint(2000, 180);
        System.out.printf("粗搜索最优瞄准点: (%.2f, %.2f), 期望得分: %.2f%n",
            coarse[0], coarse[1], coarse[2]);

        double[] refined = solver.refineOptimalAimPoint(coarse[0], coarse[1], 5.0, 30);
        System.out.printf("精搜索最优瞄准点: (%.2f, %.2f), 期望得分: %.2f%n",
            refined[0], refined[1], refined[2]);

        // 打印低分辨率热力图
        System.out.println("\n期望得分热力图:");
        solver.printHeatMap(40);
    }
}

算法复杂度分析

维度 分析
蒙特卡洛模拟 单次期望得分计算 $O(N)$,$N$ 为采样次数。精度随 $N$ 增加按 $O(1/\sqrt{N})$ 收敛。
网格积分 $O((R/\Delta)^2)$,$R$ 为靶面半径,$\Delta$ 为网格步长。适合快速相对比较。
最优搜索 粗搜索 $O(S \cdot N)$,$S$ 为候选点数;精搜索 $O(I \cdot 8 \cdot N)$,$I$ 为迭代次数。
空间复杂度 $O(1)$,仅使用常数额外空间。

扩展思考

  1. 不同精准度策略:sigma较小的精准玩家应瞄准高分风险区(如三倍20);sigma较大的休闲玩家应瞄准更保守的区域(如牛眼附近),以最大化期望得分。
  2. 动态规划收官:501分制飞镖比赛中,最后阶段的剩余分数决策可以用动态规划建模,求解最优收分路径。
  3. 对手建模:在对抗场景中,可以结合对手的瞄准习惯与落点数据,用贝叶斯推断更新其精准度分布。

总结

飞镖游戏的最优靶区选择问题将几何坐标计算、概率密度积分与贪心搜索融为一体。通过蒙特卡洛模拟估计期望得分,结合局部精细搜索找到全局最优瞄准点,我们不仅实现了一个有趣的飞镖分析工具,更展示了如何在不确定环境下做出最优决策——这套方法论同样适用于投资组合优化、广告投放等现实世界问题。