每日算法 — 使用java实现1024:滑动合并与BFS最优策略

游戏简介

1024是一款经典的数字滑动合并益智游戏。游戏在一个4×4的方格中进行,玩家通过上下左右四个方向滑动,将所有数字方块向指定方向移动。当两个相同数字的方块碰撞时,会合并成它们的和。游戏目标是凑出数字1024。本文将用Java实现完整的游戏逻辑,并深入讲解BFS状态搜索寻找最优操作序列的算法。

核心规则与数据结构

游戏规则

  • 棋盘为4×4的二维网格
  • 每次滑动后,空白位置会随机生成一个数字2或4
  • 相同数字碰撞时合并,合并后的方块在当前回合不再参与合并
  • 当棋盘填满且无法合并时,游戏结束
  • 成功生成1024即为胜利

数据结构选择

使用二维数组int[4][4]表示棋盘状态。每个状态可以用一个64位整数压缩存储(每个格子4bit,共16×4=64bit),便于状态去重和BFS搜索。

public class Board1024 {
    private int[][] grid;
    private int score;
    private static final int SIZE = 4;
    private static final Random RANDOM = new Random();

    public Board1024() {
        this.grid = new int[SIZE][SIZE];
        this.score = 0;
        spawnTile();
        spawnTile();
    }

    // 将棋盘状态压缩为long,用于BFS状态去重
    public long toLongState() {
        long state = 0;
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                int val = grid[i][j];
                // 用4bit存储每个格子的指数:0表示空,1表示2,2表示4,...,n表示2^n
                int exp = (val == 0) ? 0 : (32 - Integer.numberOfLeadingZeros(val));
                state |= ((long) exp << ((i * SIZE + j) * 4));
            }
        }
        return state;
    }
}

滑动合并算法

滑动合并是游戏的核心操作。以向左滑动为例,算法步骤如下:

  1. 移除空白:将每行的非零数字向左压缩
  2. 合并相邻:从左到右扫描,相邻相同数字合并
  3. 再次压缩:合并后可能产生新的空白,再次左移
  4. 生成新块:如果棋盘发生变化,在空白处随机生成2或4
public class Board1024 {
    // ... 其他代码

    /**
     * 向左滑动并合并
     * @return 是否发生了移动或合并
     */
    public boolean moveLeft() {
        boolean moved = false;
        for (int i = 0; i < SIZE; i++) {
            // 第一步:提取非零元素
            int[] row = new int[SIZE];
            int idx = 0;
            for (int j = 0; j < SIZE; j++) {
                if (grid[i][j] != 0) {
                    row[idx++] = grid[i][j];
                }
            }

            // 第二步:合并相邻相同数字
            int[] merged = new int[SIZE];
            int mIdx = 0;
            for (int j = 0; j < idx; j++) {
                if (j + 1 < idx && row[j] == row[j + 1]) {
                    merged[mIdx++] = row[j] * 2;
                    score += row[j] * 2;
                    j++; // 跳过已合并的下一个
                } else {
                    merged[mIdx++] = row[j];
                }
            }

            // 第三步:写回棋盘
            for (int j = 0; j < SIZE; j++) {
                int newVal = (j < mIdx) ? merged[j] : 0;
                if (grid[i][j] != newVal) {
                    moved = true;
                }
                grid[i][j] = newVal;
            }
        }

        if (moved) {
            spawnTile();
        }
        return moved;
    }

    /**
     * 通过矩阵旋转复用左滑逻辑实现其他方向
     */
    public boolean move(Direction dir) {
        // 顺时针旋转矩阵,使目标方向变为左方
        int rotations = dir.getRotations();
        for (int r = 0; r < rotations; r++) {
            rotateClockwise();
        }
        boolean moved = moveLeft();
        // 旋转回来
        for (int r = 0; r < (4 - rotations) % 4; r++) {
            rotateClockwise();
        }
        return moved;
    }

    private void rotateClockwise() {
        int[][] newGrid = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                newGrid[j][SIZE - 1 - i] = grid[i][j];
            }
        }
        grid = newGrid;
    }

    /**
     * 在空白位置随机生成2(概率90%)或4(概率10%)
     */
    private void spawnTile() {
        List<int[]> empty = new ArrayList<>();
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (grid[i][j] == 0) {
                    empty.add(new int[]{i, j});
                }
            }
        }
        if (!empty.isEmpty()) {
            int[] pos = empty.get(RANDOM.nextInt(empty.size()));
            grid[pos[0]][pos[1]] = (RANDOM.nextDouble() < 0.9) ? 2 : 4;
        }
    }
}

enum Direction {
    LEFT(0), UP(3), RIGHT(2), DOWN(1);

    private final int rotations;

    Direction(int rotations) {
        this.rotations = rotations;
    }

    public int getRotations() {
        return rotations;
    }
}

BFS状态搜索最优策略

BFS(广度优先搜索)可以用来寻找从当前状态到达目标状态(生成1024)的最短操作序列。由于状态空间巨大,实际应用中需要结合状态压缩和访问集合进行剪枝。

状态空间分析

4×4棋盘最多有16个格子,每个格子的数字最大为1024(2^10),因此理论上状态数非常庞大。但实际游戏中, reachable states 远小于理论值。使用long压缩状态后,可以用HashSet<Long>进行高效去重。

BFS算法实现

import java.util.*;

public class Solver1024 {

    /**
     * BFS搜索从当前状态到目标值的最短操作序列
     * @param board 当前棋盘
     * @param target 目标值(如1024)
     * @return 最优操作序列,无解返回null
     */
    public List<Direction> solveBFS(Board1024 board, int target) {
        Queue<StateNode> queue = new LinkedList<>();
        Set<Long> visited = new HashSet<>();

        long initState = board.toLongState();
        queue.offer(new StateNode(initState, new ArrayList<>()));
        visited.add(initState);

        int maxDepth = 8; // 限制搜索深度,避免状态爆炸

        while (!queue.isEmpty()) {
            StateNode current = queue.poll();

            if (current.moves.size() >= maxDepth) {
                continue;
            }

            // 尝试四个方向
            for (Direction dir : Direction.values()) {
                Board1024 clone = new Board1024(current.state);
                if (clone.move(dir)) {
                    // 检查是否达到目标
                    if (clone.contains(target)) {
                        List<Direction> result = new ArrayList<>(current.moves);
                        result.add(dir);
                        return result;
                    }

                    long newState = clone.toLongState();
                    if (!visited.contains(newState)) {
                        visited.add(newState);
                        List<Direction> newMoves = new ArrayList<>(current.moves);
                        newMoves.add(dir);
                        queue.offer(new StateNode(newState, newMoves));
                    }
                }
            }
        }

        return null; // 在限制深度内未找到解
    }

    /**
     * 贪心启发式策略:选择使评估函数得分最高的方向
     */
    public Direction greedyBestMove(Board1024 board) {
        Direction bestDir = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Direction dir : Direction.values()) {
            Board1024 clone = new Board1024(board);
            if (clone.move(dir)) {
                double score = evaluate(clone);
                if (score > bestScore) {
                    bestScore = score;
                    bestDir = dir;
                }
            }
        }

        return bestDir;
    }

    /**
     * 评估函数:综合多种启发式指标
     */
    private double evaluate(Board1024 board) {
        double score = 0;
        int[][] grid = board.getGrid();

        // 1. 空格数量(越多越好)
        int emptyCount = 0;
        for (int[] row : grid) {
            for (int val : row) {
                if (val == 0) emptyCount++;
            }
        }
        score += emptyCount * 100;

        // 2. 单调性奖励(大数字在角落,呈单调递减)
        score += calculateMonotonicity(grid) * 10;

        // 3. 最大数字值
        score += board.getMaxTile();

        // 4. 平滑度惩罚(相邻数字差异大则惩罚)
        score -= calculateSmoothness(grid);

        return score;
    }

    private double calculateMonotonicity(int[][] grid) {
        double[] totals = new double[4]; // 四个方向的单调性

        // 左右方向
        for (int i = 0; i < 4; i++) {
            int current = 0;
            int next = current + 1;
            while (next < 4) {
                while (next < 4 && grid[i][next] == 0) next++;
                if (next >= 4) break;
                double currentVal = grid[i][current] == 0 ? 0 : Math.log(grid[i][current]) / Math.log(2);
                double nextVal = grid[i][next] == 0 ? 0 : Math.log(grid[i][next]) / Math.log(2);
                if (currentVal > nextVal) {
                    totals[0] += nextVal - currentVal;
                } else {
                    totals[1] += currentVal - nextVal;
                }
                current = next;
                next++;
            }
        }

        // 上下方向(类似逻辑,省略以节省空间)
        // ...

        return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]);
    }

    private double calculateSmoothness(int[][] grid) {
        double smoothness = 0;
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (grid[i][j] == 0) continue;
                double val = Math.log(grid[i][j]) / Math.log(2);
                // 检查右边和下面
                if (j + 1 < 4 && grid[i][j + 1] != 0) {
                    smoothness += Math.abs(val - Math.log(grid[i][j + 1]) / Math.log(2));
                }
                if (i + 1 < 4 && grid[i + 1][j] != 0) {
                    smoothness += Math.abs(val - Math.log(grid[i + 1][j]) / Math.log(2));
                }
            }
        }
        return smoothness;
    }
}

class StateNode {
    long state;
    List<Direction> moves;

    StateNode(long state, List<Direction> moves) {
        this.state = state;
        this.moves = moves;
    }
}

完整可运行示例

public class Game1024 {
    public static void main(String[] args) {
        Board1024 board = new Board1024();
        Solver1024 solver = new Solver1024();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            board.print();

            if (board.contains(1024)) {
                System.out.println("恭喜!你已成功合成1024!");
                break;
            }

            if (!board.canMove()) {
                System.out.println("游戏结束!无法继续移动。");
                break;
            }

            // 使用贪心策略给出建议
            Direction advice = solver.greedyBestMove(board);
            System.out.println("AI建议方向: " + advice);

            System.out.print("输入方向 (W/A/S/D): ");
            String input = scanner.nextLine().trim().toUpperCase();

            Direction dir = switch (input) {
                case "W" -> Direction.UP;
                case "A" -> Direction.LEFT;
                case "S" -> Direction.DOWN;
                case "D" -> Direction.RIGHT;
                default -> advice;
            };

            if (!board.move(dir)) {
                System.out.println("该方向无法移动,请重试!");
            }
        }

        scanner.close();
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
单次滑动合并 O(n²),n为棋盘边长 O(n)
BFS状态搜索 O(b^d),b为分支因子(≤4),d为搜索深度 O(b^d)
状态压缩/解压 O(n²) O(1)
贪心评估 O(n²) O(1)

总结

本文实现了1024游戏的完整Java版本,核心亮点包括:

  • 滑动合并算法:通过提取-合并-压缩三步法实现,利用矩阵旋转统一四个方向的处理逻辑
  • 状态压缩:将4×4棋盘压缩为64位整数,支持高效的BFS状态去重
  • BFS最优搜索:在有限深度内搜索到达目标的最短操作序列
  • 贪心启发式策略:综合空格数、单调性、平滑度等指标评估局面,为AI决策提供依据

读者可以通过调整评估函数的权重、增加搜索深度、或引入蒙特卡洛模拟进一步提升AI水平。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注