每日算法 — 使用java实现高尔夫:Dijkstra地形寻路与贝塞尔曲线击球路径

引言

高尔夫是一项将精确控制与策略规划完美结合的运动。球员不仅需要考虑球杆的选择和击球的力度,更需要规划球在球道上的行进路径——不同的地形(长草区、沙坑、水域)会对球的滚动产生截然不同的影响。如何在复杂地形上找到最优的击球路径,使总杆数最少?

本文用Java实现高尔夫球场路径规划系统,涵盖地形网格建模、Dijkstra最短路径算法、贝塞尔曲线击球轨迹模拟与力度控制策略。

问题建模与算法思路

高尔夫球场建模

将球场离散化为二维网格,每个格子具有:
– 地形类型:球道(fairway,滚动阻力小)、长草区(rough,阻力大)、沙坑(bunker,极难打出)、水域(water,罚杆)、果岭(green,可推杆)。
– 通行代价:球在该地形上每移动1米的等效代价(杆数等价)。
– 高度信息:坡度影响球的滚动距离和方向。

核心算法思路

  1. 地形网格Dijkstra:从球当前位置到洞口,在不同地形上寻找代价最低的行进路线。这里”代价”不是欧几里得距离,而是预估杆数。
  2. 贝塞尔曲线击球:每次击球的轨迹用三次贝塞尔曲线建模,考虑初始方向、力度和地形阻力。
  3. 分段规划:将整场球分解为多段”当前位置 → 下一落点”的子问题,每段选择最优的击球方向和力度。
  4. 动态规划(可选):记忆化不同位置的剩余杆数期望,用于长距离全局优化。

完整Java实现

import java.util.*;

/**
 * 高尔夫:Dijkstra地形寻路与贝塞尔曲线击球路径
 * 核心算法:地形加权图 + Dijkstra最短路径 + 三次贝塞尔曲线轨迹模拟
 */
public class GolfCourseSolver {

    // 地形类型与通行代价(每米的等效杆数代价)
    static final int FAIRWAY = 0;    // 球道
    static final int ROUGH = 1;      // 长草区
    static final int BUNKER = 2;     // 沙坑
    static final int WATER = 3;      // 水域(不可通行)
    static final int GREEN = 4;      // 果岭
    static final int HOLE = 5;       // 球洞

    static final double[] TERRAIN_COST = {1.0, 2.5, 5.0, Double.MAX_VALUE, 0.5, 0.0};
    static final double[] TERRAIN_ROLL = {0.85, 0.45, 0.15, 0.0, 0.95, 1.0}; // 滚动系数
    static final String[] TERRAIN_NAME = {"球道", "长草区", "沙坑", "水域", "果岭", "球洞"};

    // 球场参数
    int width, height;
    int[][] terrain;      // 地形网格
    double[][] heightMap; // 高度图(米)
    double gridSize;      // 每个网格的实际大小(米)

    // 球洞位置
    int holeR, holeC;

    public GolfCourseSolver(int width, int height, double gridSize) {
        this.width = width;
        this.height = height;
        this.gridSize = gridSize;
        this.terrain = new int[height][width];
        this.heightMap = new double[height][width];
    }

    public void setTerrain(int r, int c, int type) {
        terrain[r][c] = type;
        if (type == HOLE) {
            holeR = r;
            holeC = c;
        }
    }

    public void setHeight(int r, int c, double h) {
        heightMap[r][c] = h;
    }

    /**
     * 节点类用于Dijkstra
     */
    static class Node implements Comparable<Node> {
        int r, c;
        double cost;
        Node(int r, int c, double cost) {
            this.r = r;
            this.c = c;
            this.cost = cost;
        }
        public int compareTo(Node o) {
            return Double.compare(this.cost, o.cost);
        }
    }

    /**
     * Dijkstra算法:在地形网格上寻找从起点到球洞的最小代价路径
     * 代价 = 距离 × 地形通行代价系数
     */
    public List<int[]> findOptimalPath(int startR, int startC) {
        double[][] dist = new double[height][width];
        boolean[][] visited = new boolean[height][width];
        int[][] parentR = new int[height][width];
        int[][] parentC = new int[height][width];

        for (double[] row : dist) Arrays.fill(row, Double.MAX_VALUE);
        dist[startR][startC] = 0;

        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.add(new Node(startR, startC, 0));

        int[][] dirs = {{-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,1}, {1,-1}, {1,0}, {1,1}};

        while (!pq.isEmpty()) {
            Node cur = pq.poll();
            if (visited[cur.r][cur.c]) continue;
            visited[cur.r][cur.c] = true;

            if (cur.r == holeR && cur.c == holeC) break;

            for (int[] d : dirs) {
                int nr = cur.r + d[0];
                int nc = cur.c + d[1];
                if (nr < 0 || nr >= height || nc < 0 || nc >= width) continue;
                if (terrain[nr][nc] == WATER) continue; // 水域不可通行

                double moveDist = Math.sqrt(d[0]*d[0] + d[1]*d[1]) * gridSize;
                double terrainCost = (TERRAIN_COST[terrain[cur.r][cur.c]] + TERRAIN_COST[terrain[nr][nc]]) / 2.0;
                double heightPenalty = Math.abs(heightMap[nr][nc] - heightMap[cur.r][cur.c]) * 0.3;
                double newCost = cur.cost + moveDist * terrainCost + heightPenalty;

                if (newCost < dist[nr][nc]) {
                    dist[nr][nc] = newCost;
                    parentR[nr][nc] = cur.r;
                    parentC[nr][nc] = cur.c;
                    pq.add(new Node(nr, nc, newCost));
                }
            }
        }

        // 回溯路径
        List<int[]> path = new ArrayList<>();
        int r = holeR, c = holeC;
        while (r != startR || c != startC) {
            path.add(0, new int[]{r, c});
            int pr = parentR[r][c];
            int pc = parentC[r][c];
            if (pr == r && pc == c) break; // 无路径
            r = pr;
            c = pc;
        }
        path.add(0, new int[]{startR, startC});
        return path;
    }

    /**
     * 三次贝塞尔曲线:计算击球轨迹
     * P0: 起点, P1: 控制点1(初始方向延伸), P2: 控制点2(落点方向), P3: 落点
     */
    public List<double[]> bezierCurve(double[] p0, double[] p1, double[] p2, double[] p3, int segments) {
        List<double[]> points = new ArrayList<>();
        for (int i = 0; i <= segments; i++) {
            double t = i / (double) segments;
            double mt = 1 - t;
            double x = mt*mt*mt*p0[0] + 3*mt*mt*t*p1[0] + 3*mt*t*t*p2[0] + t*t*t*p3[0];
            double y = mt*mt*mt*p0[1] + 3*mt*mt*t*p1[1] + 3*mt*t*t*p2[1] + t*t*t*p3[1];
            points.add(new double[]{x, y});
        }
        return points;
    }

    /**
     * 模拟击球:给定起点、目标落点、力度,返回球的实际轨迹
     * 考虑地形阻力和高度变化
     */
    public List<double[]> simulateShot(double startX, double startY, double targetX, double targetY, double power) {
        // 力度决定最大飞行距离(假设1.0力度对应250米)
        double maxDistance = power * 250.0;
        double actualDistance = Math.sqrt(Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2));

        // 如果目标超过最大距离,按比例缩放
        double ratio = Math.min(1.0, maxDistance / actualDistance);
        double realTargetX = startX + (targetX - startX) * ratio;
        double realTargetY = startY + (targetY - startY) * ratio;

        // 控制点:P1沿初始方向延伸,P2靠近落点
        double midX = (startX + realTargetX) / 2;
        double midY = (startY + realTargetY) / 2;
        double[] p0 = {startX, startY};
        double[] p1 = {startX + (midX - startX) * 0.5, startY + (midY - startY) * 0.5 + actualDistance * 0.15};
        double[] p2 = {realTargetX + (midX - realTargetX) * 0.3, realTargetY + (midY - realTargetY) * 0.3 + actualDistance * 0.05};
        double[] p3 = {realTargetX, realTargetY};

        List<double[]> trajectory = bezierCurve(p0, p1, p2, p3, 50);

        // 考虑落地后的滚动
        double[] last = trajectory.get(trajectory.size() - 1);
        int gridR = (int) (last[1] / gridSize);
        int gridC = (int) (last[0] / gridSize);
        gridR = Math.max(0, Math.min(height - 1, gridR));
        gridC = Math.max(0, Math.min(width - 1, gridC));
        double rollFactor = TERRAIN_ROLL[terrain[gridR][gridC]];

        // 滚动方向沿最后一段轨迹方向
        double[] prev = trajectory.get(trajectory.size() - 2);
        double dirX = last[0] - prev[0];
        double dirY = last[1] - prev[1];
        double dirLen = Math.sqrt(dirX * dirX + dirY * dirY);
        if (dirLen > 0) {
            dirX /= dirLen;
            dirY /= dirLen;
        }
        double rollDist = actualDistance * rollFactor * 0.1;
        double[] finalPos = {last[0] + dirX * rollDist, last[1] + dirY * rollDist};
        trajectory.add(finalPos);

        return trajectory;
    }

    /**
     * 规划从起点到洞口的完整击球序列
     */
    public List<ShotPlan> planFullGame(int startR, int startC) {
        List<ShotPlan> shots = new ArrayList<>();
        double curX = startC * gridSize;
        double curY = startR * gridSize;
        double holeX = holeC * gridSize;
        double holeY = holeR * gridSize;
        int maxShots = 10;

        for (int shot = 0; shot < maxShots; shot++) {
            double distToHole = Math.sqrt(Math.pow(holeX - curX, 2) + Math.pow(holeY - curY, 2));
            if (distToHole < gridSize * 2) {
                shots.add(new ShotPlan(curX, curY, holeX, holeY, 0.1, "推杆进洞"));
                break;
            }

            // 使用Dijkstra找最优路径上的下一个中间目标
            int gridR = (int) (curY / gridSize);
            int gridC = (int) (curX / gridSize);
            List<int[]> path = findOptimalPath(gridR, gridC);
            if (path.size() < 2) break;

            // 选择路径上的一个合适落点(不要太远)
            int targetIdx = Math.min(path.size() - 1, 5 + shot * 2);
            int[] targetGrid = path.get(targetIdx);
            double targetX = targetGrid[1] * gridSize;
            double targetY = targetGrid[0] * gridSize;

            // 计算所需力度
            double dist = Math.sqrt(Math.pow(targetX - curX, 2) + Math.pow(targetY - curY, 2));
            double power = Math.min(1.0, dist / 200.0 + 0.2);

            String club = selectClub(power);
            shots.add(new ShotPlan(curX, curY, targetX, targetY, power, club));

            // 模拟击球,更新当前位置
            List<double[]> traj = simulateShot(curX, curY, targetX, targetY, power);
            double[] finalPos = traj.get(traj.size() - 1);
            curX = finalPos[0];
            curY = finalPos[1];
        }
        return shots;
    }

    private String selectClub(double power) {
        if (power > 0.9) return "一号木杆(Driver)";
        if (power > 0.7) return "三号木杆(3-Wood)";
        if (power > 0.5) return "五号铁杆(5-Iron)";
        if (power > 0.3) return "七号铁杆(7-Iron)";
        if (power > 0.15) return "劈起杆(Pitching Wedge)";
        return "推杆(Putter)";
    }

    /**
     * 击球计划
     */
    static class ShotPlan {
        double fromX, fromY, toX, toY;
        double power;
        String club;
        ShotPlan(double fx, double fy, double tx, double ty, double p, String c) {
            this.fromX = fx; this.fromY = fy;
            this.toX = tx; this.toY = ty;
            this.power = p; this.club = c;
        }
    }

    /**
     * 可视化球场和路径
     */
    public void visualizeCourse() {
        System.out.println("球场地形图(F=球道 R=长草 B=沙坑 W=水域 G=果岭 H=洞)");
        char[] symbols = {'F', 'R', 'B', 'W', 'G', 'H'};
        for (int r = 0; r < height; r++) {
            for (int c = 0; c < width; c++) {
                System.out.print(symbols[terrain[r][c]]);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        // 创建一个示例球场(20x30网格,每格10米)
        GolfCourseSolver course = new GolfCourseSolver(30, 20, 10.0);

        // 初始化地形:中间是球道,两侧是长草区,有沙坑和水域障碍
        for (int r = 0; r < 20; r++) {
            for (int c = 0; c < 30; c++) {
                if (c >= 12 && c <= 17) {
                    course.setTerrain(r, c, FAIRWAY);
                } else if (c >= 9 && c <= 20) {
                    course.setTerrain(r, c, ROUGH);
                } else {
                    course.setTerrain(r, c, BUNKER);
                }
            }
        }

        // 添加水域障碍
        for (int c = 10; c < 20; c++) course.setTerrain(8, c, WATER);
        for (int c = 11; c < 19; c++) course.setTerrain(9, c, WATER);

        // 添加沙坑
        course.setTerrain(5, 14, BUNKER);
        course.setTerrain(5, 15, BUNKER);
        course.setTerrain(14, 13, BUNKER);
        course.setTerrain(14, 16, BUNKER);

        // 果岭和球洞
        for (int r = 17; r < 20; r++) {
            for (int c = 13; c <= 16; c++) {
                course.setTerrain(r, c, GREEN);
            }
        }
        course.setTerrain(18, 14, HOLE);

        // 起点
        int startR = 1, startC = 14;

        course.visualizeCourse();

        System.out.println("\n=== Dijkstra最优路径 ===");
        List<int[]> path = course.findOptimalPath(startR, startC);
        System.out.println("路径节点数: " + path.size());
        for (int i = 0; i < Math.min(path.size(), 10); i++) {
            int[] p = path.get(i);
            System.out.printf("  节点%d: (%d, %d) %s%n", i, p[0], p[1], TERRAIN_NAME[course.terrain[p[0]][p[1]]]);
        }
        if (path.size() > 10) System.out.println("  ...");

        System.out.println("\n=== 完整击球计划 ===");
        List<ShotPlan> shots = course.planFullGame(startR, startC);
        for (int i = 0; i < shots.size(); i++) {
            ShotPlan s = shots.get(i);
            double dist = Math.sqrt(Math.pow(s.toX - s.fromX, 2) + Math.pow(s.toY - s.fromY, 2));
            System.out.printf("第%d杆: %s, 距离%.1fm, 力度%.0f%%%n",
                i + 1, s.club, dist, s.power * 100);
        }
    }
}

算法复杂度分析

维度 分析
Dijkstra最短路径 $O(V \log V + E \log V)$,使用优先队列。$V = W \times H$ 为网格数,$E \approx 8V$ 为边数。
贝塞尔曲线生成 $O(S)$,$S$ 为采样段数,与精度相关。
击球模拟 $O(S + R)$,$S$ 为飞行轨迹段数,$R$ 为滚动计算。
完整击球序列 $O(K \cdot (V \log V + S))$,$K$ 为预计杆数,每杆需重新规划路径。
空间复杂度 $O(V)$ 存储距离数组和父节点信息。

扩展思考

  1. A*算法加速:当只需要近似最优路径时,可用A*算法结合欧几里得距离启发式,显著减少搜索节点数。
  2. 风力建模:加入风向和风力参数,用向量叠加修正贝塞尔曲线的控制点。
  3. 强化学习策略:将每杆击球视为马尔可夫决策过程,用Q-Learning或策略梯度学习长期最优策略。
  4. 三维地形扩展:引入真实DEM高程数据,用更精细的物理引擎模拟球的弹跳和滚动。

总结

高尔夫路径规划问题展示了图论算法与几何曲线在实际场景中的综合应用。通过Dijkstra算法在地形加权网格上寻找最优球道,再用贝塞尔曲线建模击球轨迹,我们不仅实现了一个有趣的高尔夫策略系统,更展示了如何将抽象的算法理论转化为解决现实世界路径规划问题的实用工具。这套方法论可推广到无人机航线规划、自动驾驶路径选择等领域。