每日算法 — 使用java实现小鳄鱼爱洗澡:管道网络流与最小割最大流

引言

小鳄鱼爱洗澡(Where is My Water?)是一款经典的物理解谜游戏:玩家通过挖掘泥土、引导水流,帮助小鳄鱼Swampy顺利洗上澡。游戏中每关的目标是使足够的水流到达目标位置。要将关卡求解问题抽象为算法问题,关键在于将管道布局和障碍物建模为图论中的网络流模型。本文将用Java实现基于网络流建模的关卡求解器,讲解Dinic最大流算法与最小割定理,将物理水流问题转化为图论优化问题高效求解。

核心算法设计

1. 关卡图建模

将游戏地图离散化为网格图G=(V,E):
– 每个网格单元为一个顶点,属性包括:泥土(可挖掘)、岩石(不可通过)、管道(有向边)、水源(超级源点)、目标点(超级汇点)
– 相邻可通网格之间建立边,容量为1(单格单位水流量)或更大(宽管道)
– 水源连接到所有出水网格,容量为INF
– 所有目标网格连接到超级汇点,容量为需求水量

2. Dinic最大流算法

Dinic算法是求解单源单汇最大流问题的高效算法,时间复杂度O(V²E)(一般网络)或O(E√V)(单位容量网络)。核心步骤:

  1. BFS分层:从源点出发构建层次图,只保留从低到高的边
  2. DFS阻塞流:在层次图上沿增广路推送流量,直到不存在增广路
  3. 重复:当BFS无法到达汇点时,当前流即为最大流

3. 最小割与关卡判定

根据最大流最小割定理:最大流值 = 最小割容量。若最大流值 ≥ 目标需水量,则关卡可通关;否则不可通关。最小割集对应的是”最瓶颈”的管道/泥土位置——即关卡的难点所在。

Java实现

import java.util.*;

/**
 * 小鳄鱼爱洗澡关卡求解器
 * 核心算法:网络流建模 + Dinic最大流 + 最小割分析
 */
public class CrocodileBathSolver {

    // ==================== Dinic最大流核心 ====================

    static class Edge {
        int to;      // 目标顶点
        int rev;     // 反向边索引
        int cap;     // 剩余容量

        Edge(int to, int rev, int cap) {
            this.to = to;
            this.rev = rev;
            this.cap = cap;
        }
    }

    static class Dinic {
        int n;                      // 顶点数
        List<Edge>[] graph;         // 邻接表
        int[] level;                // BFS层次
        int[] iter;                 // 当前弧优化

        @SuppressWarnings("unchecked")
        Dinic(int n) {
            this.n = n;
            graph = new ArrayList[n];
            for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
            level = new int[n];
            iter = new int[n];
        }

        /**
         * 添加有向边及其反向边(残量网络)
         * @param from 起点
         * @param to 终点
         * @param cap 容量
         */
        void addEdge(int from, int to, int cap) {
            graph[from].add(new Edge(to, graph[to].size(), cap));
            graph[to].add(new Edge(from, graph[from].size() - 1, 0)); // 反向边初始容量为0
        }

        /**
         * BFS构建层次图
         * @param s 源点
         * @param t 汇点
         * @return 是否可达汇点
         */
        boolean bfs(int s, int t) {
            Arrays.fill(level, -1);
            Queue<Integer> queue = new LinkedList<>();
            level[s] = 0;
            queue.add(s);
            while (!queue.isEmpty()) {
                int v = queue.poll();
                for (Edge e : graph[v]) {
                    if (e.cap > 0 && level[e.to] < 0) {
                        level[e.to] = level[v] + 1;
                        queue.add(e.to);
                    }
                }
            }
            return level[t] >= 0;
        }

        /**
         * DFS在层次图上寻找阻塞流
         * @param v 当前顶点
         * @param t 汇点
         * @param f 当前可推送流量
         * @return 实际推送流量
         */
        int dfs(int v, int t, int f) {
            if (v == t) return f;
            for (int i = iter[v]; i < graph[v].size(); i++) {
                Edge e = graph[v].get(i);
                if (e.cap > 0 && level[v] < level[e.to]) {
                    int d = dfs(e.to, t, Math.min(f, e.cap));
                    if (d > 0) {
                        e.cap -= d;
                        graph[e.to].get(e.rev).cap += d;
                        return d;
                    }
                }
                iter[v]++;
            }
            return 0;
        }

        /**
         * 计算从s到t的最大流
         */
        int maxFlow(int s, int t) {
            int flow = 0;
            final int INF = Integer.MAX_VALUE;
            while (bfs(s, t)) {
                Arrays.fill(iter, 0);
                int f;
                while ((f = dfs(s, t, INF)) > 0) {
                    flow += f;
                }
            }
            return flow;
        }

        /**
         * 获取最小割的S侧顶点(从源点可达)
         * 用于分析关卡瓶颈
         */
        boolean[] minCutS side(int s) {
            boolean[] visited = new boolean[n];
            Queue<Integer> queue = new LinkedList<>();
            visited[s] = true;
            queue.add(s);
            while (!queue.isEmpty()) {
                int v = queue.poll();
                for (Edge e : graph[v]) {
                    if (e.cap > 0 && !visited[e.to]) {
                        visited[e.to] = true;
                        queue.add(e.to);
                    }
                }
            }
            return visited;
        }
    }

    // ==================== 关卡地图建模 ====================

    static class Cell {
        int r, c;           // 网格坐标
        char type;          // '.':空地 '#':泥土 'R':岩石 'W':水源 'D':目标 'P':管道
        int pipeCap;        // 管道容量(宽管道容量>1)

        Cell(int r, int c, char type) {
            this.r = r; this.c = c; this.type = type;
            this.pipeCap = (type == 'P') ? 2 : 1; // 默认管道容量2,普通通道容量1
        }
    }

    private final int rows, cols;
    private final Cell[][] grid;
    private final List<Cell> waterSources = new ArrayList<>();
    private final List<Cell> targets = new ArrayList<>();

    public CrocodileBathSolver(char[][] map) {
        this.rows = map.length;
        this.cols = map[0].length;
        this.grid = new Cell[rows][cols];
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                grid[r][c] = new Cell(r, c, map[r][c]);
                if (map[r][c] == 'W') waterSources.add(grid[r][c]);
                if (map[r][c] == 'D') targets.add(grid[r][c]);
            }
        }
    }

    /**
     * 将网格坐标映射为图顶点编号
     */
    int nodeId(int r, int c) {
        return r * cols + c;
    }

    /**
     * 构建网络流图
     * @param dugCells 已挖掘的泥土格子集合(可作为通道)
     * @return 包含源点汇点的Dinic实例
     */
    Dinic buildFlowNetwork(Set<Integer> dugCells) {
        int totalNodes = rows * cols + 2;
        int source = rows * cols;     // 超级源点
        int sink = rows * cols + 1;   // 超级汇点
        Dinic dinic = new Dinic(totalNodes);

        int[] dr = {-1, 1, 0, 0};
        int[] dc = {0, 0, -1, 1};

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                Cell cell = grid[r][c];
                int u = nodeId(r, c);

                // 岩石不可通过,跳过
                if (cell.type == 'R') continue;

                // 泥土未挖掘则不可通过
                if (cell.type == '#' && !dugCells.contains(u)) continue;

                // 水源连接到超级源点
                if (cell.type == 'W') {
                    dinic.addEdge(source, u, Integer.MAX_VALUE / 4);
                }

                // 目标点连接到超级汇点(需求水量设为3)
                if (cell.type == 'D') {
                    dinic.addEdge(u, sink, 3);
                }

                // 四连通邻接
                for (int d = 0; d < 4; d++) {
                    int nr = r + dr[d], nc = c + dc[d];
                    if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
                    Cell neighbor = grid[nr][nc];
                    int v = nodeId(nr, nc);

                    if (neighbor.type == 'R') continue;
                    if (neighbor.type == '#' && !dugCells.contains(v)) continue;

                    // 边容量取决于管道类型或默认1
                    int cap = Math.min(cell.pipeCap, neighbor.pipeCap);
                    if (cell.type == 'P' || neighbor.type == 'P') {
                        cap = Math.max(cap, 2);
                    }
                    dinic.addEdge(u, v, cap);
                }
            }
        }
        return dinic;
    }

    /**
     * 求解当前关卡:计算最大可到达目标的水量
     * @param dugCells 已挖掘格子
     * @return 最大流值(即可送达目标的水量)
     */
    public int solveLevel(Set<Integer> dugCells) {
        if (waterSources.isEmpty() || targets.isEmpty()) return 0;
        int source = rows * cols;
        int sink = rows * cols + 1;
        Dinic dinic = buildFlowNetwork(dugCells);
        return dinic.maxFlow(source, sink);
    }

    /**
     * 关卡可行性判定与瓶颈分析
     * @param requiredWater 通关所需水量
     * @param dugCells 已挖掘格子
     * @return 分析结果
     */
    public LevelAnalysis analyzeLevel(int requiredWater, Set<Integer> dugCells) {
        int source = rows * cols;
        int sink = rows * cols + 1;
        Dinic dinic = buildFlowNetwork(dugCells);
        int maxFlow = dinic.maxFlow(source, sink);

        boolean canComplete = maxFlow >= requiredWater;

        // 最小割分析:找到瓶颈位置
        boolean[] sSide = dinic.minCutS side(source);
        List<Cell> bottlenecks = new ArrayList<>();
        int[] dr = {-1, 1, 0, 0};
        int[] dc = {0, 0, -1, 1};

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                int u = nodeId(r, c);
                if (!sSide[u]) continue;
                for (int d = 0; d < 4; d++) {
                    int nr = r + dr[d], nc = c + dc[d];
                    if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
                    int v = nodeId(nr, nc);
                    // S侧到T侧的边即为割边
                    if (!sSide[v] && grid[r][c].type != 'R') {
                        bottlenecks.add(grid[r][c]);
                    }
                }
            }
        }

        return new LevelAnalysis(maxFlow, requiredWater, canComplete, bottlenecks);
    }

    static class LevelAnalysis {
        int maxFlow;
        int requiredWater;
        boolean canComplete;
        List<Cell> bottlenecks;

        LevelAnalysis(int maxFlow, int requiredWater, boolean canComplete, List<Cell> bottlenecks) {
            this.maxFlow = maxFlow;
            this.requiredWater = requiredWater;
            this.canComplete = canComplete;
            this.bottlenecks = bottlenecks;
        }
    }

    // ==================== 主程序与测试 ====================

    public static void main(String[] args) {
        // 定义一个简单关卡地图
        // .:空地  #:泥土  R:岩石  W:水源  D:目标  P:管道
        char[][] map = {
            {'#', '#', '#', '#', '#', '#'},
            {'#', 'W', '.', 'P', '.', '#'},
            {'#', '#', '#', '.', '#', '#'},
            {'#', '.', 'P', '.', 'D', '#'},
            {'#', '#', '#', '#', '#', '#'},
        };

        CrocodileBathSolver solver = new CrocodileBathSolver(map);

        System.out.println("===== 小鳄鱼爱洗澡关卡求解器 =====\n");
        System.out.println("关卡地图:");
        for (char[] row : map) {
            System.out.println("  " + new String(row));
        }
        System.out.println();

        // 场景1:未挖掘任何泥土
        Set<Integer> noDig = new HashSet<>();
        int flow1 = solver.solveLevel(noDig);
        System.out.println("场景1(未挖掘): 最大可送达水量 = " + flow1);

        // 场景2:挖通关键泥土格子 (2,3) 和 (2,1)
        Set<Integer> digKey = new HashSet<>();
        digKey.add(solver.nodeId(2, 3)); // 中间通道
        digKey.add(solver.nodeId(2, 1)); // 左侧通道(可选)
        int flow2 = solver.solveLevel(digKey);
        System.out.println("场景2(挖通关键路径): 最大可送达水量 = " + flow2);

        // 关卡分析
        LevelAnalysis analysis = solver.analyzeLevel(3, digKey);
        System.out.println("\n关卡分析报告:");
        System.out.println("  最大流/需求量: " + analysis.maxFlow + "/" + analysis.requiredWater);
        System.out.println("  是否可通关: " + (analysis.canComplete ? "是" : "否"));
        System.out.println("  瓶颈位置数: " + analysis.bottlenecks.size());
        for (Cell b : analysis.bottlenecks) {
            System.out.println("    -> (" + b.r + "," + b.c + ") 类型=" + b.type);
        }
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
建图 O(R×C) O(R×C) R行C列网格
Dinic BFS分层 O(V+E) O(V) V≈R×C, E≈4R×C
Dinic DFS阻塞流 O(E×f) O(V) f为单位容量下的流量
整体最大流 O(V²E) O(V+E) 一般网络最坏情况
最小割分析 O(V+E) O(V) 一次BFS遍历残量网络

算法扩展思路

  1. 多源多汇扩展:引入超级源点和超级汇点后,Dinic天然支持多水源和多目标点的场景,可扩展至复杂管网设计。
  2. 最小费用最大流:若挖掘不同泥土有不同成本,可为边赋予费用,使用最小费用最大流算法寻找成本最低的通关方案。
  3. 时变水流模拟:将网络流扩展为时间扩展网络(Time-Expanded Network),模拟水流的动态传播过程,更贴合真实物理。

总结

本文将小鳄鱼爱洗澡的关卡求解问题抽象为网络流模型,通过Dinic算法高效计算最大可送达水量,并利用最大流最小割定理定位关卡瓶颈。该框架展示了如何将物理直觉问题转化为严谨的图论优化问题,是算法在游戏关卡设计与自动测试中的典型应用。