每日算法 — 使用java实现是男人就下一百层:动态规划与贪心避障路径

引言

“是男人就下一百层”是一款经典的垂直下落类益智游戏,玩家控制角色在不断上升的钉板间隙中向下跳跃,目标是尽可能多地穿越楼层。本文将用Java实现该游戏的核心AI,重点讲解动态规划(DP)最优路径与贪心避障策略的结合应用。通过将连续的游戏场景离散化为状态网格,AI能够在实时环境中计算出生存概率最高的下落轨迹。

问题建模

游戏场景抽象

将游戏场景建模为一个 $M \times N$ 的离散网格:
– $M$ 为楼层数量(如100层)
– 每层包含若干”安全平台”(可站立区域)和”危险区域”(钉板/空隙)
– 角色每次可左移/右移/停留,然后自动下落一层

状态定义

定义状态 $dp[i][j]$ 为:角色位于第 $i$ 层第 $j$ 列时,能够成功到达底部的最大生存概率(或最大可下落层数)。

状态转移方程:
$$dp[i][j] = \max_{k \in {-1, 0, 1}} { dp[i+1][j+k] } + 1$$

其中 $k$ 表示水平移动方向(左/中/右),且目标位置必须是安全平台。

核心算法设计

算法一:逆向动态规划

从底部向上逆向计算每个位置的最优值,确保每个状态只依赖已计算的下一层状态。

算法二:贪心局部避障

在实时游戏中,玩家只能看到下方有限层数(如5层)。此时采用贪心策略:在可见范围内选择使 $dp$ 值最大的即时动作。

算法三:A*启发式搜索(可选扩展)

当需要穿越复杂地形时,可将每层平台视为图节点,使用A*搜索全局最优路径,启发函数为曼哈顿距离加上安全平台密度。

Java实现

import java.util.*;

/**
 * 是男人就下一百层 - 动态规划与贪心避障路径
 */
public class Down100Floors {

    // 游戏常量
    private static final int FLOORS = 100;      // 总层数
    private static final int COLS = 20;         // 每层的列数
    private static final int VISIBLE_RANGE = 5; // 玩家可见层数
    private static final double DANGER_PROB = 0.3; // 危险区域生成概率

    // 地形类型
    enum TileType { SAFE, DANGER, LADDER }

    static class Tile {
        TileType type;
        int floor;    // 楼层
        int col;      // 列号

        Tile(int floor, int col, TileType type) {
            this.floor = floor;
            this.col = col;
            this.type = type;
        }
    }

    private Tile[][] grid;       // 游戏地图
    private int[][] dp;          // DP表:dp[i][j]表示从(i,j)出发最大可下落层数
    private boolean[][] reachable; // 可达性标记
    private Random random;

    public Down100Floors() {
        this.random = new Random(42); // 固定种子便于复现
        generateMap();
        computeDP();
    }

    /**
     * 随机生成游戏地图
     * 每层包含连续的安全平台,确保存在可行路径
     */
    private void generateMap() {
        grid = new Tile[FLOORS][COLS];

        for (int f = 0; f < FLOORS; f++) {
            // 生成至少一个安全平台段
            int safeStart = random.nextInt(COLS - 5);
            int safeLen = 3 + random.nextInt(4); // 安全平台长度3-6

            for (int c = 0; c < COLS; c++) {
                if (c >= safeStart && c < safeStart + safeLen) {
                    grid[f][c] = new Tile(f, c, TileType.SAFE);
                } else if (random.nextDouble() < 0.1) {
                    grid[f][c] = new Tile(f, c, TileType.LADDER); // 10%梯子可穿越
                } else {
                    grid[f][c] = new Tile(f, c, TileType.DANGER);
                }
            }
        }

        // 确保底部是安全的
        for (int c = 0; c < COLS; c++) {
            grid[FLOORS - 1][c] = new Tile(FLOORS - 1, c, TileType.SAFE);
        }
    }

    /**
     * 逆向动态规划:计算从每个位置出发的最大可下落层数
     */
    private void computeDP() {
        dp = new int[FLOORS][COLS];
        reachable = new boolean[FLOORS][COLS];

        // 初始化底部
        for (int c = 0; c < COLS; c++) {
            dp[FLOORS - 1][c] = 1;
            reachable[FLOORS - 1][c] = true;
        }

        // 逆向递推
        for (int f = FLOORS - 2; f >= 0; f--) {
            for (int c = 0; c < COLS; c++) {
                if (grid[f][c].type == TileType.DANGER) {
                    dp[f][c] = 0;
                    reachable[f][c] = false;
                    continue;
                }

                int best = 0;
                // 考虑三种水平移动:左、中、右
                for (int dc = -1; dc <= 1; dc++) {
                    int nc = c + dc;
                    if (nc >= 0 && nc < COLS && reachable[f + 1][nc]) {
                        best = Math.max(best, dp[f + 1][nc]);
                    }
                }

                if (best > 0) {
                    dp[f][c] = best + 1;
                    reachable[f][c] = true;
                } else {
                    dp[f][c] = 1; // 只能停留在此层
                    reachable[f][c] = true;
                }
            }
        }
    }

    /**
     * 贪心AI:基于当前可见范围选择最优动作
     * @param currentFloor 当前楼层
     * @param currentCol 当前列
     * @return 最优水平移动方向:-1左, 0中, 1右
     */
    public int greedyMove(int currentFloor, int currentCol) {
        if (currentFloor >= FLOORS - 1) return 0;

        int bestDir = 0;
        int bestScore = -1;

        // 在可见范围内评估每个方向
        for (int dc = -1; dc <= 1; dc++) {
            int nc = currentCol + dc;
            if (nc < 0 || nc >= COLS) continue;
            if (grid[currentFloor][nc].type == TileType.DANGER) continue;

            // 评估该方向的长期收益(基于DP值)
            int score = evaluateDirection(currentFloor, nc, VISIBLE_RANGE);

            // 加入安全平台密度作为辅助启发
            score += countSafeNeighbors(currentFloor, nc) * 2;

            if (score > bestScore) {
                bestScore = score;
                bestDir = dc;
            }
        }

        return bestDir;
    }

    /**
     * 评估某个方向在可见范围内的累计DP值
     */
    private int evaluateDirection(int floor, int col, int range) {
        int score = 0;
        int cf = floor;
        int cc = col;

        for (int i = 0; i < range && cf + i < FLOORS; i++) {
            if (cc >= 0 && cc < COLS && reachable[cf + i][cc]) {
                score += dp[cf + i][cc];
            } else {
                break; // 不可达,停止评估
            }
        }
        return score;
    }

    /**
     * 统计相邻位置的安全平台数量
     */
    private int countSafeNeighbors(int floor, int col) {
        int count = 0;
        for (int dc = -1; dc <= 1; dc++) {
            int nc = col + dc;
            if (nc >= 0 && nc < COLS && grid[floor][nc].type != TileType.DANGER) {
                count++;
            }
        }
        return count;
    }

    /**
     * 带AI的游戏模拟
     * @param startCol 起始列
     * @return 成功下落的层数
     */
    public int simulateAiGame(int startCol) {
        int floor = 0;
        int col = startCol;
        int steps = 0;

        while (floor < FLOORS - 1 && steps < FLOORS * 2) {
            if (grid[floor][col].type == TileType.DANGER) {
                break; // 死亡
            }

            int move = greedyMove(floor, col);
            col += move;

            // 边界检查
            col = Math.max(0, Math.min(COLS - 1, col));

            // 下落一层
            floor++;
            steps++;

            // 如果落在危险区域但旁边有梯子,有概率存活(简化)
            if (grid[floor][col].type == TileType.DANGER) {
                // 检查是否可以横向移动到安全区域
                boolean saved = false;
                for (int dc = -1; dc <= 1 && !saved; dc++) {
                    int nc = col + dc;
                    if (nc >= 0 && nc < COLS && grid[floor][nc].type != TileType.DANGER) {
                        col = nc;
                        saved = true;
                    }
                }
                if (!saved) break;
            }
        }

        return floor; // 返回到达的最深楼层
    }

    /**
     * 打印地图和DP值(调试用)
     */
    public void printMap() {
        System.out.println("游戏地图(S=安全, D=危险, L=梯子):");
        for (int f = 0; f < Math.min(FLOORS, 20); f++) {
            System.out.printf("层%3d: ", f);
            for (int c = 0; c < COLS; c++) {
                char ch = grid[f][c].type == TileType.SAFE ? 'S' :
                         grid[f][c].type == TileType.LADDER ? 'L' : 'D';
                System.out.print(ch);
            }
            System.out.println();
        }
    }

    /**
     * 获取从起点出发的全局最优路径(基于DP表)
     */
    public List<int[]> getOptimalPath(int startCol) {
        List<int[]> path = new ArrayList<>();
        int f = 0, c = startCol;

        while (f < FLOORS - 1 && reachable[f][c]) {
            path.add(new int[]{f, c});

            int bestDir = 0;
            int bestDp = dp[f + 1][c];

            for (int dc = -1; dc <= 1; dc++) {
                int nc = c + dc;
                if (nc >= 0 && nc < COLS && f + 1 < FLOORS && dp[f + 1][nc] > bestDp) {
                    bestDp = dp[f + 1][nc];
                    bestDir = dc;
                }
            }

            c += bestDir;
            f++;
        }
        path.add(new int[]{f, c});
        return path;
    }

    public static void main(String[] args) {
        Down100Floors game = new Down100Floors();
        game.printMap();

        System.out.println("\n=== AI模拟结果 ===");
        int totalReached = 0;
        int games = 100;
        int startCol = 5;

        for (int i = 0; i < games; i++) {
            // 每次重新生成地图(使用不同种子)
            game = new Down100Floors();
            int reached = game.simulateAiGame(startCol);
            totalReached += reached;
        }

        System.out.printf("运行 %d 局,平均到达 %.1f 层(总共 %d 层)%n", 
            games, (double) totalReached / games, FLOORS);

        // 展示最优路径
        game = new Down100Floors();
        List<int[]> optimal = game.getOptimalPath(startCol);
        System.out.printf("\n从第%d列出发的DP最优路径长度: %d 步%n", startCol, optimal.size());
    }
}

复杂度分析

指标 复杂度 说明
地图生成 $O(M \times N)$ $M$层$N$列的随机初始化
DP预计算 $O(M \times N \times 3)$ 逆向递推,每层每列评估3个方向
贪心决策 $O(V \times N)$ $V$为可见范围,每步常数时间
空间复杂度 $O(M \times N)$ 地图、DP表、可达性标记

动态规划的优势在于一次计算、多次查询:预处理后每次决策仅需 $O(1)$ 查表。贪心策略在局部可见范围内近似全局最优,适合实时性要求高的游戏场景。

扩展思考

  1. 部分可观测性:引入”迷雾”机制,玩家只能看到下方2-3层。此时需要使用POMDP(部分可观测马尔可夫决策过程)建模,结合信念状态更新做出决策
  2. 动态地图:当钉板移动时,状态空间变为时变系统,需使用在线规划算法(如RTDP,实时动态规划)
  3. 多目标优化:除生存外加入”收集金币”目标,转化为多目标最短路径问题,可用A*的变体求解

总结

本文通过Java实现了”是男人就下一百层”游戏的AI核心,展示了动态规划与贪心策略的结合范式:DP负责离线全局最优解的计算,贪心负责在线局部决策的实时响应。这种分层架构在游戏AI设计中具有普遍意义——全局规划提供方向性指导,局部策略处理实时不确定性。代码中完整的地图生成、DP预计算和路径模拟模块可直接扩展为完整游戏项目。