每日算法 — 使用java实现漂流:流场向量建模与最优航道搜索

一、问题背景

漂流运动中,皮划艇借助水流动力在河道中前进。河道中不同位置的水流速度和方向各异,同时存在漩涡、暗礁等障碍。漂流者需要选择一条最优航道,在避开障碍物的同时最大化前进速度。本文用Java建立二维流场模型,并用A*搜索算法求解最优漂流路径。

二、核心算法思路

2.1 流场建模

将河道离散化为二维网格,每个网格单元具有:
– 水流速度向量:(vx, vy),表示该位置的水流方向和速度
– 水深:影响皮划艇的阻力
– 障碍物标记:标识不可通行的区域(岩石、浅滩等)

水流场使用多个涡旋源的叠加来模拟复杂水流:

v(x, y) = v_base + Σ vortex_i(x, y)

其中每个涡旋的速度场为:

vx = -Γ * (y - cy) / (2π * r²)
vy =  Γ * (x - cx) / (2π * r²)

Γ 为涡旋强度,(cx, cy) 为涡旋中心,r 为到中心的距离。

2.2 皮划艇运动模型

皮划艇的速度由水流速度和划桨速度共同决定:

v_boat = v_current + v_paddle

划桨速度受限于人体功率输出,且逆流划行时效率降低。当水流速度超过划桨速度时,皮划艇只能顺流漂移。

2.3 A*最优路径搜索

将漂流航道搜索建模为图搜索问题:
– 节点:河道网格单元
– 边:相邻单元之间的可移动连接
– 边权:通过该边所需的时间(距离 / 有效速度)

A*算法使用启发函数 h(n) 估计从当前节点到终点的代价。本文使用欧几里得距离除以最大可能速度作为启发函数,保证可采纳性。

三、Java实现

import java.util.*;

/**
 * 漂流流场模拟与A*最优航道搜索
 */
public class RaftingPathfinding {

    // 河道参数
    private static final int GRID_WIDTH = 50;
    private static final int GRID_HEIGHT = 30;
    private static final double CELL_SIZE = 10; // 每格10米
    private static final double MAX_PADDLE_SPEED = 2.0; // 最大划桨速度 (m/s)
    private static final double BASE_FLOW_SPEED = 3.0;  // 基础流速 (m/s)

    /**
     * 二维向量
     */
    static class Vector2D {
        double x, y;
        Vector2D(double x, double y) { this.x = x; this.y = y; }
        double magnitude() { return Math.sqrt(x * x + y * y); }
        Vector2D add(Vector2D other) { return new Vector2D(x + other.x, y + other.y); }
        Vector2D scale(double s) { return new Vector2D(x * s, y * s); }
    }

    /**
     * 涡旋源
     */
    static class Vortex {
        double cx, cy;     // 中心位置(网格坐标)
        double strength;   // 涡旋强度
        double radius;     // 影响半径

        Vortex(double cx, double cy, double strength, double radius) {
            this.cx = cx; this.cy = cy;
            this.strength = strength; this.radius = radius;
        }

        /**
         * 计算位置处的涡旋速度
         */
        Vector2D velocityAt(double x, double y) {
            double dx = x - cx;
            double dy = y - cy;
            double r2 = dx * dx + dy * dy;
            double r = Math.sqrt(r2);

            if (r > radius || r < 0.1) return new Vector2D(0, 0);

            double decay = Math.exp(-r / radius);
            double factor = strength * decay / (2 * Math.PI * r2 + 0.1);
            return new Vector2D(-dy * factor, dx * factor);
        }
    }

    /**
     * 流场
     */
    static class FlowField {
        Vector2D[][] velocities;
        boolean[][] obstacles;
        int width, height;
        List<Vortex> vortices = new ArrayList<>();

        FlowField(int width, int height) {
            this.width = width;
            this.height = height;
            velocities = new Vector2D[width][height];
            obstacles = new boolean[width][height];

            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    velocities[i][j] = new Vector2D(0, 0);
                }
            }
        }

        void addVortex(Vortex v) {
            vortices.add(v);
        }

        void setObstacle(int x, int y) {
            if (x >= 0 && x < width && y >= 0 && y < height) {
                obstacles[x][y] = true;
            }
        }

        /**
         * 计算并缓存全流场
         */
        void computeFlowField() {
            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    // 基础流向:从左到右,带轻微波动
                    double baseVx = BASE_FLOW_SPEED + Math.sin(j * 0.3) * 0.5;
                    double baseVy = Math.cos(i * 0.2) * 0.3;
                    Vector2D v = new Vector2D(baseVx, baseVy);

                    // 叠加涡旋
                    for (Vortex vtx : vortices) {
                        v = v.add(vtx.velocityAt(i, j));
                    }

                    velocities[i][j] = v;
                }
            }
        }

        Vector2D getVelocity(int x, int y) {
            if (x < 0 || x >= width || y < 0 || y >= height) return new Vector2D(0, 0);
            return velocities[x][y];
        }

        boolean isObstacle(int x, int y) {
            if (x < 0 || x >= width || y < 0 || y >= height) return true;
            return obstacles[x][y];
        }
    }

    /**
     * A*搜索节点
     */
    static class SearchNode implements Comparable<SearchNode> {
        int x, y;
        double g; // 从起点到当前节点的实际代价
        double h; // 启发式估计代价
        SearchNode parent;

        SearchNode(int x, int y, double g, double h, SearchNode parent) {
            this.x = x; this.y = y;
            this.g = g; this.h = h;
            this.parent = parent;
        }

        double f() { return g + h; }

        @Override
        public int compareTo(SearchNode other) {
            return Double.compare(this.f(), other.f());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof SearchNode)) return false;
            SearchNode other = (SearchNode) o;
            return x == other.x && y == other.y;
        }

        @Override
        public int hashCode() {
            return x * 1000 + y;
        }
    }

    /**
     * A*路径搜索
     */
    static class AStarPathfinder {

        /**
         * 计算通过两格之间边的时间代价
         */
        double edgeCost(FlowField field, int x1, int y1, int x2, int y2) {
            if (field.isObstacle(x2, y2)) return Double.MAX_VALUE;

            Vector2D flow = field.getVelocity(x1, y1);
            double dx = x2 - x1;
            double dy = y2 - y1;
            double dist = Math.sqrt(dx * dx + dy * dy) * CELL_SIZE;

            // 单位方向
            double dirX = dx / Math.sqrt(dx * dx + dy * dy);
            double dirY = dy / Math.sqrt(dx * dx + dy * dy);

            // 水流在前进方向的分量
            double flowAlong = flow.x * dirX + flow.y * dirY;
            // 垂直分量导致偏移损耗
            double flowAcross = Math.abs(flow.x * dirY - flow.y * dirX);

            // 有效速度 = 水流助力 + 划桨速度 - 偏移损耗
            double effectiveSpeed = flowAlong + MAX_PADDLE_SPEED - flowAcross * 0.5;
            effectiveSpeed = Math.max(effectiveSpeed, 0.5); // 最小速度保证

            return dist / effectiveSpeed;
        }

        /**
         * 启发函数:欧几里得距离 / 最大可能速度
         */
        double heuristic(int x, int y, int goalX, int goalY) {
            double dx = (goalX - x) * CELL_SIZE;
            double dy = (goalY - y) * CELL_SIZE;
            double dist = Math.sqrt(dx * dx + dy * dy);
            double maxSpeed = BASE_FLOW_SPEED + MAX_PADDLE_SPEED;
            return dist / maxSpeed;
        }

        /**
         * 搜索最优路径
         */
        List<int[]> findPath(FlowField field, int startX, int startY, int goalX, int goalY) {
            PriorityQueue<SearchNode> openSet = new PriorityQueue<>();
            Set<String> closedSet = new HashSet<>();
            Map<String, SearchNode> nodeMap = new HashMap<>();

            SearchNode start = new SearchNode(startX, startY, 0,
                    heuristic(startX, startY, goalX, goalY), null);
            openSet.offer(start);
            nodeMap.put(startX + "," + startY, start);

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

            while (!openSet.isEmpty()) {
                SearchNode current = openSet.poll();
                String key = current.x + "," + current.y;

                if (closedSet.contains(key)) continue;
                closedSet.add(key);

                if (current.x == goalX && current.y == goalY) {
                    return reconstructPath(current);
                }

                for (int[] dir : directions) {
                    int nx = current.x + dir[0];
                    int ny = current.y + dir[1];
                    String nKey = nx + "," + ny;

                    if (closedSet.contains(nKey)) continue;
                    if (field.isObstacle(nx, ny)) continue;

                    double edgeCost = edgeCost(field, current.x, current.y, nx, ny);
                    if (edgeCost == Double.MAX_VALUE) continue;

                    double newG = current.g + edgeCost;
                    SearchNode neighbor = nodeMap.get(nKey);

                    if (neighbor == null || newG < neighbor.g) {
                        double h = heuristic(nx, ny, goalX, goalY);
                        SearchNode newNode = new SearchNode(nx, ny, newG, h, current);
                        openSet.offer(newNode);
                        nodeMap.put(nKey, newNode);
                    }
                }
            }

            return new ArrayList<>(); // 无路径
        }

        List<int[]> reconstructPath(SearchNode goal) {
            List<int[]> path = new ArrayList<>();
            SearchNode current = goal;
            while (current != null) {
                path.add(new int[]{current.x, current.y});
                current = current.parent;
            }
            Collections.reverse(path);
            return path;
        }
    }

    /**
     * 生成示例河道
     */
    static FlowField generateRiver() {
        FlowField field = new FlowField(GRID_WIDTH, GRID_HEIGHT);

        // 添加涡旋
        field.addVortex(new Vortex(15, 10, 15, 8));
        field.addVortex(new Vortex(30, 20, -12, 7));
        field.addVortex(new Vortex(40, 12, 10, 6));

        // 设置障碍物(模拟河中岩石和岸壁)
        Random rand = new Random(123);
        for (int i = 0; i < 15; i++) {
            int ox = 5 + rand.nextInt(GRID_WIDTH - 10);
            int oy = rand.nextInt(GRID_HEIGHT);
            field.setObstacle(ox, oy);
            if (oy > 0) field.setObstacle(ox, oy - 1);
        }

        // 岸壁
        for (int i = 0; i < GRID_WIDTH; i++) {
            if (Math.sin(i * 0.2) > 0.5) {
                field.setObstacle(i, 0);
                field.setObstacle(i, 1);
            }
            if (Math.cos(i * 0.15) > 0.5) {
                field.setObstacle(i, GRID_HEIGHT - 1);
                field.setObstacle(i, GRID_HEIGHT - 2);
            }
        }

        field.computeFlowField();
        return field;
    }

    public static void main(String[] args) {
        FlowField river = generateRiver();
        System.out.println("=== 漂流流场模拟与A*最优航道搜索 ===\n");
        System.out.printf("河道网格: %d x %d (每格%.0fm)%n", GRID_WIDTH, GRID_HEIGHT, CELL_SIZE);
        System.out.printf("河道尺寸: %.0fm x %.0fm%n\n",
                GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE);

        // 起点和终点
        int startX = 2, startY = GRID_HEIGHT / 2;
        int goalX = GRID_WIDTH - 3, goalY = GRID_HEIGHT / 2;

        System.out.printf("起点: (%d, %d)%n", startX, startY);
        System.out.printf("终点: (%d, %d)%n\n", goalX, goalY);

        // A*搜索
        AStarPathfinder finder = new AStarPathfinder();
        List<int[]> path = finder.findPath(river, startX, startY, goalX, goalY);

        if (path.isEmpty()) {
            System.out.println("未找到可行路径!");
            return;
        }

        System.out.printf("最优路径节点数: %d%n", path.size());

        // 计算路径统计
        double totalDist = 0;
        double totalTime = 0;
        double maxFlowSpeed = 0;

        for (int i = 0; i < path.size(); i++) {
            int[] p = path.get(i);
            Vector2D v = river.getVelocity(p[0], p[1]);
            maxFlowSpeed = Math.max(maxFlowSpeed, v.magnitude());

            if (i > 0) {
                int[] prev = path.get(i - 1);
                double dx = (p[0] - prev[0]) * CELL_SIZE;
                double dy = (p[1] - prev[1]) * CELL_SIZE;
                double dist = Math.sqrt(dx * dx + dy * dy);
                totalDist += dist;
                totalTime += finder.edgeCost(river, prev[0], prev[1], p[0], p[1]);
            }
        }

        System.out.printf("总路程: %.1f 米%n", totalDist);
        System.out.printf("预计通行时间: %.1f 秒%n", totalTime);
        System.out.printf("平均速度: %.2f m/s%n", totalDist / totalTime);
        System.out.printf("路径上最大流速: %.2f m/s%n", maxFlowSpeed);

        // 可视化输出
        System.out.println("\n航道图 ('S'=起点, 'G'=终点, '*'=路径, '#'=障碍, '.'=水域):");
        char[][] map = new char[GRID_WIDTH][GRID_HEIGHT];
        for (int y = 0; y < GRID_HEIGHT; y++) {
            for (int x = 0; x < GRID_WIDTH; x++) {
                if (river.isObstacle(x, y)) map[x][y] = '#';
                else map[x][y] = '.';
            }
        }
        for (int[] p : path) {
            map[p[0]][p[1]] = '*';
        }
        map[startX][startY] = 'S';
        map[goalX][goalY] = 'G';

        for (int y = 0; y < GRID_HEIGHT; y++) {
            for (int x = 0; x < GRID_WIDTH; x++) {
                System.out.print(map[x][y]);
            }
            System.out.println();
        }
    }
}

四、算法复杂度分析

  • 流场计算:O(W * H * V),W 为宽度,H 为高度,V 为涡旋数量
  • A*搜索:O(E log V),其中 V = W * H,E ≈ 8V(8邻域连接)
  • 空间复杂度:O(W * H),存储流场和搜索状态

五、扩展思考

  1. 动态流场:引入时间维度,模拟潮汐和季节性水流变化
  2. 多人协同:多艘皮划艇编队漂流,使用群体智能算法协调队形
  3. 风险建模:将急流和漩涡按危险等级分类,在路径规划中引入风险权重