每日算法 — 使用java实现消灭星星:BFS连通块检测与贪心消除策略

引言

消灭星星(PopStar)是一款风靡全球的消除类益智游戏。玩家通过点击相邻的同色星星块来消除它们,消除的块越多得分越高,最终目标是清空棋盘获得最高分数。看似简单的规则背后,蕴含着丰富的算法设计空间:如何高效检测连通块?怎样选择最优消除顺序以最大化得分?本文将用Java完整实现消灭星星的核心算法,重点讲解BFS连通块检测与贪心消除策略。

游戏规则与算法建模

规则抽象

游戏棋盘通常是一个 10×10 的矩形网格,每个格子包含一种颜色(用整数1~5表示)。当玩家点击某个格子时,所有与其上下左右相邻且颜色相同的格子构成一个连通块。若连通块大小不少于2,则这些格子被消除,上方格子下落填补空缺,右侧整列向左移动压缩。

核心算法问题

  1. 连通块检测:给定点击位置,快速找出所有同色相邻格子
  2. 重力模拟:消除后让上方星星下落、右侧列左移
  3. 消除策略:在每一步选择最优的连通块进行消除,以最大化最终得分

BFS连通块检测算法

连通块检测本质上是图遍历问题。将棋盘每个格子视为图的节点,相邻同色格子之间存在边。从点击位置出发,使用BFS或DFS遍历即可找到整个连通块。BFS在此场景下更为直观,且天然适合寻找最短路径层数(虽然本题不需要)。

import java.util.*;

/**
 * 消灭星星游戏核心算法实现
 * 重点:BFS连通块检测 + 贪心消除策略
 */
public class PopStarSolver {

    // 棋盘大小
    private static final int ROWS = 10;
    private static final int COLS = 10;
    // 颜色种类数
    private static final int COLORS = 5;

    // 四个方向:上、下、左、右
    private static final int[][] DIRS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    // 棋盘状态,0表示空,1~5表示不同颜色
    private int[][] board;

    public PopStarSolver() {
        this.board = new int[ROWS][COLS];
    }

    /**
     * 使用BFS检测从(r,c)出发的连通块
     * @param r 起始行
     * @param c 起始列
     * @return 连通块中所有格子的坐标列表
     */
    public List<int[]> findConnectedComponent(int r, int c) {
        List<int[]> component = new ArrayList<>();

        // 边界检查与空格子检查
        if (!isValid(r, c) || board[r][c] == 0) {
            return component;
        }

        int color = board[r][c];
        boolean[][] visited = new boolean[ROWS][COLS];
        ArrayDeque<int[]> queue = new ArrayDeque<>();

        queue.offer(new int[]{r, c});
        visited[r][c] = true;
        component.add(new int[]{r, c});

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

            // 遍历四个邻居
            for (int[] dir : DIRS) {
                int nr = cr + dir[0];
                int nc = cc + dir[1];

                // 检查边界、未访问、同色
                if (isValid(nr, nc) && !visited[nr][nc] && board[nr][nc] == color) {
                    visited[nr][nc] = true;
                    queue.offer(new int[]{nr, nc});
                    component.add(new int[]{nr, nc});
                }
            }
        }

        return component;
    }

    private boolean isValid(int r, int c) {
        return r >= 0 && r < ROWS && c >= 0 && c < COLS;
    }

    /**
     * 计算连通块的得分:消除n个块的得分公式通常为 n * n * 5
     * 例如消除2块得20分,消除5块得125分,鼓励大规模消除
     */
    public int calculateScore(int blockCount) {
        if (blockCount < 2) return 0;
        return blockCount * blockCount * 5;
    }

重力模拟与棋盘压缩

消除连通块后,需要模拟两个物理效果:
1. 纵向重力:每列中被消除的格子上方星星下落
2. 横向压缩:若某列完全为空,右侧所有列向左移动

    /**
     * 消除指定连通块,并应用重力与压缩
     * @param component 要消除的连通块坐标列表
     * @return 本次消除获得的分数
     */
    public int eliminate(List<int[]> component) {
        if (component.size() < 2) return 0;

        // 标记消除位置
        for (int[] pos : component) {
            board[pos[0]][pos[1]] = 0;
        }

        // 纵向重力:每列从下往上收集非零元素
        for (int c = 0; c < COLS; c++) {
            int writeRow = ROWS - 1; // 从底部开始写入
            for (int r = ROWS - 1; r >= 0; r--) {
                if (board[r][c] != 0) {
                    board[writeRow][c] = board[r][c];
                    if (writeRow != r) {
                        board[r][c] = 0;
                    }
                    writeRow--;
                }
            }
            // 剩余位置已经是0(因为消除时标记了)
            while (writeRow >= 0) {
                board[writeRow--][c] = 0;
            }
        }

        // 横向压缩:将非空列向左移动
        int writeCol = 0;
        for (int c = 0; c < COLS; c++) {
            // 检查该列是否全空
            boolean isEmpty = true;
            for (int r = 0; r < ROWS; r++) {
                if (board[r][c] != 0) {
                    isEmpty = false;
                    break;
                }
            }

            if (!isEmpty) {
                if (writeCol != c) {
                    for (int r = 0; r < ROWS; r++) {
                        board[r][writeCol] = board[r][c];
                        board[r][c] = 0;
                    }
                }
                writeCol++;
            }
        }

        return calculateScore(component.size());
    }

贪心消除策略

消灭星星是一个典型的NP-hard问题(可归约为分组问题),对于大规模棋盘难以求得全局最优解。实践中采用贪心策略:每一步选择当前能消除的最大连通块。这种策略虽然无法保证全局最优,但在多数情况下表现良好。

    /**
     * 贪心策略:找出当前棋盘中最大的连通块并消除
     * @return 本次操作得分,若无可消除块返回-1表示游戏结束
     */
    public int greedyEliminate() {
        List<int[]> bestComponent = new ArrayList<>();
        boolean[][] visited = new boolean[ROWS][COLS];

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != 0 && !visited[r][c]) {
                    // BFS找连通块,同时标记visited避免重复计算
                    List<int[]> comp = findConnectedComponentWithVisited(r, c, visited);
                    if (comp.size() > bestComponent.size()) {
                        bestComponent = comp;
                    }
                }
            }
        }

        if (bestComponent.size() < 2) {
            return -1; // 游戏结束
        }

        return eliminate(bestComponent);
    }

    /**
     * 带visited标记的BFS,用于批量查找所有连通块
     */
    private List<int[]> findConnectedComponentWithVisited(int r, int c, boolean[][] visited) {
        List<int[]> component = new ArrayList<>();
        int color = board[r][c];
        ArrayDeque<int[]> queue = new ArrayDeque<>();

        queue.offer(new int[]{r, c});
        visited[r][c] = true;
        component.add(new int[]{r, c});

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            for (int[] dir : DIRS) {
                int nr = cur[0] + dir[0];
                int nc = cur[1] + dir[1];
                if (isValid(nr, nc) && !visited[nr][nc] && board[nr][nc] == color) {
                    visited[nr][nc] = true;
                    queue.offer(new int[]{nr, nc});
                    component.add(new int[]{nr, nc});
                }
            }
        }
        return component;
    }

完整可运行程序

    /**
     * 初始化随机棋盘
     */
    public void initRandomBoard() {
        Random rand = new Random();
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                board[r][c] = rand.nextInt(COLORS) + 1;
            }
        }
    }

    /**
     * 打印当前棋盘状态
     */
    public void printBoard() {
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                System.out.print(board[r][c] == 0 ? ". " : board[r][c] + " ");
            }
            System.out.println();
        }
        System.out.println();
    }

    /**
     * 运行完整游戏流程
     */
    public int playGame() {
        int totalScore = 0;
        int step = 0;

        System.out.println("=== 初始棋盘 ===");
        printBoard();

        while (true) {
            int score = greedyEliminate();
            if (score < 0) break;

            totalScore += score;
            step++;
            System.out.println("第" + step + "步消除,得分: " + score);
            printBoard();
        }

        // 清空奖励:若棋盘全空额外加1000分
        boolean isEmpty = true;
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != 0) {
                    isEmpty = false;
                    break;
                }
            }
        }

        if (isEmpty) {
            totalScore += 1000;
            System.out.println("棋盘清空!奖励1000分");
        }

        System.out.println("游戏结束!总步数: " + step + ",总得分: " + totalScore);
        return totalScore;
    }

    public static void main(String[] args) {
        PopStarSolver game = new PopStarSolver();
        game.initRandomBoard();
        game.playGame();
    }
}

算法复杂度分析

操作 时间复杂度 空间复杂度 说明
BFS连通块检测 O(k) O(k) k为连通块大小
重力模拟 O(ROWS×COLS) O(1) 原地操作
贪心全遍历 O(ROWS×COLS) O(ROWS×COLS) visited数组
单步最优消除 O((ROWS×COLS)²) O(ROWS×COLS) 最坏情况需遍历全图

对于 10×10 的棋盘,每一步贪心选择的计算量在可接受范围内。若需优化,可使用并查集(Union-Find)动态维护连通块,将单步复杂度降至近似O(1)。

进阶思考:DFS vs BFS的选择

虽然本文使用BFS实现连通块检测,但在消灭星星场景中DFS同样适用。两者的区别在于:
– BFS:按层扩展,适合寻找最短路径或需要按距离分层的场景
– DFS:递归深入,代码更简洁,但可能因连通块过大导致栈溢出

对于大棋盘(如 20×20),建议仍使用BFS配合显式队列,避免递归深度限制。

总结

消灭星星游戏的Java实现涵盖了BFS图遍历、二维数组操作、贪心策略等多个经典算法主题。通过本文的代码,读者不仅能理解消除类游戏的核心机制,还能掌握如何将算法思维应用于实际游戏开发。并查集优化、A*启发式搜索寻找更优消除序列等都是值得深入探索的扩展方向。

发表回复

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