每日算法 — 使用java实现接水管:状态机管道连通与BFS水流验证

引言

接水管(Pipe Mania / Pipe Dream)是经典的益智解谜游戏,玩家需要旋转管道片段,使水源从入口顺利流到出口。本文用Java实现接水管的核心算法系统,重点讲解有限状态机管理管道方向与类型、BFS水流验证判断连通性,以及回溯搜索寻找最优旋转序列。

核心算法设计

1. 管道状态机模型

每种管道有特定的连接方向,可用4位二进制表示(上、右、下、左):

  • 直管(竖):1010(上下连通)
  • 直管(横):0101(左右连通)
  • 弯管:1001、1100、0110、0011(两两连通)
  • 十字管:1111(四向连通)
  • T型管:1110、1011、0111、1101(三通)

每个管道可以旋转0°、90°、180°、270°,形成状态机中的不同状态。

2. BFS水流验证

验证管道是否连通的核心算法:

  • 从水源入口开始BFS
  • 检查相邻管道是否有相互对接的连接方向
  • 记录所有可达的格子
  • 若出口被访问则通关成功

3. 回溯求解策略

对于需要求解最少旋转次数的关卡,使用DFS回溯:

  • 按顺序尝试每个管道的每种旋转状态
  • 用BFS快速验证当前布局是否形成通路
  • 记录找到的最小旋转次数方案
  • 剪枝:若当前旋转次数已超过已知最优解,提前返回

完整Java实现

import java.util.*;

/**
 * 接水管核心算法实现
 * 包含管道状态机、BFS水流验证与回溯求解
 */
public class PipeMania {

    // 方向定义:上(0)、右(1)、下(2)、左(3)
    static final int UP = 0, RIGHT = 1, DOWN = 2, LEFT = 3;
    static final int[] DX = {-1, 0, 1, 0};
    static final int[] DY = {0, 1, 0, -1};

    // 方向反义:检查对接时用
    static final int[] OPPOSITE = {DOWN, LEFT, UP, RIGHT};

    // 管道类型定义(原始连通方向,用4位掩码)
    static final int PIPE_STRAIGHT_V = 0b1010; // 竖直直管:上下
    static final int PIPE_STRAIGHT_H = 0b0101; // 水平直管:左右
    static final int PIPE_BEND_UR = 0b1100;    // 上右弯管
    static final int PIPE_BEND_RD = 0b0110;    // 右下弯管
    static final int PIPE_BEND_DL = 0b0011;    // 下左弯管
    static final int PIPE_BEND_LU = 0b1001;    // 左上弯管
    static final int PIPE_CROSS = 0b1111;      // 十字管

    int rows, cols;
    int[][] grid;       // 管道类型
    int[][] rotation;   // 旋转状态 0~3
    int sourceX, sourceY; // 水源入口
    int sinkX, sinkY;     // 出水口

    public PipeMania(int rows, int cols) {
        this.rows = rows;
        this.cols = cols;
        this.grid = new int[rows][cols];
        this.rotation = new int[rows][cols];
    }

    /**
     * 计算管道在某旋转状态下的实际连通方向掩码
     * 状态机核心:原始方向按旋转偏移
     */
    public int getConnectedDirections(int pipeType, int rot) {
        // 旋转相当于方向位循环右移rot次
        int mask = pipeType;
        for (int i = 0; i < rot; i++) {
            // 循环右移1位:bit3->bit0, bit0->bit1, ...
            int bit0 = mask & 1;
            mask = (mask >> 1) | (bit0 << 3);
        }
        return mask;
    }

    /**
     * 检查某方向是否连通
     */
    public boolean isConnected(int pipeType, int rot, int dir) {
        return (getConnectedDirections(pipeType, rot) & (1 << dir)) != 0;
    }

    /**
     * BFS水流验证算法
     * 从水源出发,沿连通管道传播,检查能否到达出水口
     * @return 是否成功连通,以及访问到的格子数
     */
    public FlowResult validateFlow() {
        boolean[][] visited = new boolean[rows][cols];
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{sourceX, sourceY});
        visited[sourceX][sourceY] = true;
        int reachedCells = 0;
        boolean sinkReached = false;

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            int cx = cur[0], cy = cur[1];
            reachedCells++;
            if (cx == sinkX && cy == sinkY) sinkReached = true;

            int pipeType = grid[cx][cy];
            int rot = rotation[cx][cy];

            // 遍历四个方向
            for (int dir = 0; dir < 4; dir++) {
                if (!isConnected(pipeType, rot, dir)) continue;

                int nx = cx + DX[dir];
                int ny = cy + DY[dir];

                if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue;
                if (visited[nx][ny]) continue;

                // 检查相邻管道是否在该反方向连通
                int neighborType = grid[nx][ny];
                int neighborRot = rotation[nx][ny];
                int oppositeDir = OPPOSITE[dir];

                if (isConnected(neighborType, neighborRot, oppositeDir)) {
                    visited[nx][ny] = true;
                    queue.offer(new int[]{nx, ny});
                }
            }
        }

        return new FlowResult(sinkReached, reachedCells, visited);
    }

    static class FlowResult {
        boolean sinkReached;
        int reachedCells;
        boolean[][] visited;
        FlowResult(boolean s, int r, boolean[][] v) {
            this.sinkReached = s; this.reachedCells = r; this.visited = v;
        }
    }

    /**
     * 回溯求解:寻找使水流从源到汇的最小旋转次数
     * 使用DFS + BFS验证 + 最优解剪枝
     */
    int bestRotations = Integer.MAX_VALUE;
    int[][] bestSolution = null;

    public void solveMinimumRotations(int curX, int curY, int rotCount) {
        // 剪枝1:超过已知最优解
        if (rotCount >= bestRotations) return;

        // 到达最后一个格子,验证并更新最优解
        if (curX == rows) {
            FlowResult result = validateFlow();
            if (result.sinkReached && rotCount < bestRotations) {
                bestRotations = rotCount;
                bestSolution = new int[rows][cols];
                for (int i = 0; i < rows; i++)
                    bestSolution[i] = rotation[i].clone();
            }
            return;
        }

        // 计算下一个位置
        int nextX = curX, nextY = curY + 1;
        if (nextY >= cols) { nextX++; nextY = 0; }

        // 水源和出水口位置固定,不可旋转
        if ((curX == sourceX && curY == sourceY) || (curX == sinkX && curY == sinkY)) {
            solveMinimumRotations(nextX, nextY, rotCount);
            return;
        }

        int pipeType = grid[curX][curY];
        // 十字管旋转不改变连通性,只尝试一次
        int maxRot = (pipeType == PIPE_CROSS) ? 1 : 4;

        for (int r = 0; r < maxRot; r++) {
            rotation[curX][curY] = r;
            // 剪枝2:中间验证,如果当前已不可能连通则跳过
            // (可选优化:仅在某些检查点做验证)
            solveMinimumRotations(nextX, nextY, rotCount + (r == 0 ? 0 : 1));
        }

        // 恢复状态
        rotation[curX][curY] = 0;
    }

    /**
     * 贪心启发式求解(适用于大规模关卡)
     * 从水源出发BFS,对未对齐的管道选择使对接成功的最小旋转
     */
    public boolean greedySolve() {
        boolean[][] visited = new boolean[rows][cols];
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{sourceX, sourceY});
        visited[sourceX][sourceY] = true;

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            int cx = cur[0], cy = cur[1];

            for (int dir = 0; dir < 4; dir++) {
                int nx = cx + DX[dir];
                int ny = cy + DY[dir];

                if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue;
                if (visited[nx][ny]) continue;

                // 尝试最小旋转使两管道对接
                int myPipe = grid[cx][cy];
                int myRot = rotation[cx][cy];
                int neighborPipe = grid[nx][ny];
                int oppositeDir = OPPOSITE[dir];

                if (!isConnected(myPipe, myRot, dir)) continue; // 当前不连通

                // 找使邻居在反方向连通的最小旋转
                boolean found = false;
                for (int r = 0; r < 4; r++) {
                    if (isConnected(neighborPipe, r, oppositeDir)) {
                        rotation[nx][ny] = r;
                        found = true;
                        break;
                    }
                }

                if (found) {
                    visited[nx][ny] = true;
                    queue.offer(new int[]{nx, ny});
                }
            }
        }

        return visited[sinkX][sinkY];
    }

    /**
     * 评估当前布局的"连通质量"
     * 用于关卡生成时确保有解
     */
    public double evaluateConnectivity() {
        FlowResult result = validateFlow();
        // 连通到出口的格子比例 + 是否到达出口
        double ratio = (double) result.reachedCells / (rows * cols);
        return result.sinkReached ? ratio + 1.0 : ratio;
    }

    /**
     * 关卡生成:随机放置管道,确保有解
     */
    public void generateLevel(Random rand) {
        int[] pipeTypes = {
            PIPE_STRAIGHT_V, PIPE_STRAIGHT_H,
            PIPE_BEND_UR, PIPE_BEND_RD, PIPE_BEND_DL, PIPE_BEND_LU,
            PIPE_CROSS
        };

        // 随机放置管道
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                grid[i][j] = pipeTypes[rand.nextInt(pipeTypes.length)];
                rotation[i][j] = rand.nextInt(4);
            }
        }

        // 设置水源和出水口
        sourceX = 0; sourceY = 0;
        sinkX = rows - 1; sinkY = cols - 1;

        // 确保水源和出水口至少有一个方向是开放的
        grid[sourceX][sourceY] = PIPE_BEND_RD; // 向右下开放
        rotation[sourceX][sourceY] = 0;
        grid[sinkX][sinkY] = PIPE_BEND_LU;     // 向左上开放
        rotation[sinkX][sinkY] = 0;
    }

    public void printGrid() {
        String[] chars = {"│", "─", "└", "┌", "┐", "┘", "┼"};
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                int idx = switch (grid[i][j]) {
                    case PIPE_STRAIGHT_V -> 0;
                    case PIPE_STRAIGHT_H -> 1;
                    case PIPE_BEND_UR, PIPE_BEND_RD, PIPE_BEND_DL, PIPE_BEND_LU -> 2 + rotation[i][j];
                    case PIPE_CROSS -> 6;
                    default -> 0;
                };
                System.out.print(chars[idx] + " ");
            }
            System.out.println();
        }
    }

    // 演示入口
    public static void main(String[] args) {
        PipeMania game = new PipeMania(5, 5);
        Random rand = new Random(123);
        game.generateLevel(rand);

        System.out.println("=== 接水管 初始布局 ===");
        game.printGrid();

        System.out.println("\n贪心求解...");
        boolean solved = game.greedySolve();
        FlowResult result = game.validateFlow();
        System.out.println("水源到达出水口: " + result.sinkReached);
        System.out.println("连通格子数: " + result.reachedCells);

        System.out.println("\n回溯求最优解...");
        game.bestRotations = Integer.MAX_VALUE;
        game.solveMinimumRotations(0, 0, 0);
        System.out.println("最小旋转次数: " + (game.bestRotations == Integer.MAX_VALUE ? "无解" : game.bestRotations));
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
BFS水流验证 O(n) O(n) n为地图格子数
回溯求解 O(4^n × n) O(n) 每格最多4种旋转,每次验证O(n)
贪心启发式 O(n) O(n) 一次BFS遍历
连通性评估 O(n) O(n) 同BFS验证

算法亮点总结

  • 状态机建模管道方向:用4位掩码编码连通方向,旋转操作转化为位运算循环移位,简洁高效。
  • BFS验证多场景复用:不仅用于最终通关判断,还用于贪心求解的逐层推进和关卡生成评估。
  • 剪枝优化回溯:通过最优解上界剪枝和中间状态跳过,将指数级搜索空间大幅压缩。

这套算法可进一步扩展为支持限时挑战模式(A*搜索最优操作序列)、多水源多出口网络流验证、以及基于遗传算法的自动关卡生成等高级功能。