游戏简介与算法核心
数墙(Nurikabe)是一款经典的日式逻辑谜题。玩家在矩形网格中涂黑某些格子,满足以下规则:白色格子形成连续的”岛屿”,每个岛屿恰好包含一个数字且格子数等于该数字;黑色格子形成连续的”海洋”,且不能出现2×2的全黑方块。本文用Java实现数墙求解器,讲解约束传播、Flood Fill连通性检测与回溯搜索的结合应用。
数墙规则形式化
将网格视为二维数组,每个格子有三种状态:白(ISLAND)、黑(WATER)、未知(UNKNOWN)。约束条件:
- 每个含数字的格子是一个独立岛屿的核心,该岛屿的格子数严格等于数字
- 所有白色格子必须属于某个岛屿(岛屿间不相连)
- 所有黑色格子必须连通(四连通)
- 不存在2×2的黑色格子块
- 黑色格子必须形成一个连通的整体
基础数据结构
import java.util.*;
/**
* 格子状态枚举
*/
enum CellState {
UNKNOWN('.'), // 未确定
ISLAND_WHITE('W'), // 岛屿(白色)
WATER_BLACK('#'); // 海洋(黑色)
private final char symbol;
CellState(char symbol) { this.symbol = symbol; }
public char getSymbol() { return symbol; }
}
/**
* 坐标类
*/
class Cell {
final int row, col;
Cell(int row, int col) { this.row = row; this.col = col; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Cell)) return false;
Cell c = (Cell) o;
return row == c.row && col == c.col;
}
@Override
public int hashCode() { return Objects.hash(row, col); }
@Override
public String toString() { return "(" + row + "," + col + ")"; }
}
/**
* 岛屿定义:包含数字的格子及其期望大小
*/
class Island {
final Cell seed; // 数字所在格子
final int targetSize; // 目标大小
final Set<Cell> cells = new HashSet<>(); // 当前归属该岛屿的格子
Island(Cell seed, int targetSize) {
this.seed = seed;
this.targetSize = targetSize;
this.cells.add(seed);
}
public int currentSize() { return cells.size(); }
public boolean isComplete() { return currentSize() == targetSize; }
}
/**
* 数墙棋盘
*/
class NurikabeBoard {
private final int rows, cols;
private final CellState[][] grid;
private final int[][] numbers; // 0表示无数字
private final List<Island> islands = new ArrayList<>();
public NurikabeBoard(int[][] puzzle) {
this.rows = puzzle.length;
this.cols = puzzle[0].length;
this.grid = new CellState[rows][cols];
this.numbers = new int[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
grid[r][c] = CellState.UNKNOWN;
numbers[r][c] = puzzle[r][c];
if (puzzle[r][c] > 0) {
islands.add(new Island(new Cell(r, c), puzzle[r][c]));
grid[r][c] = CellState.ISLAND_WHITE; // 数字格必为白
}
}
}
}
public CellState get(int r, int c) {
return grid[r][c];
}
public void set(int r, int c, CellState state) {
grid[r][c] = state;
}
public boolean isValidCell(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
public int getNumber(int r, int c) { return numbers[r][c]; }
public boolean hasNumber(int r, int c) { return numbers[r][c] > 0; }
public int getRows() { return rows; }
public int getCols() { return cols; }
public List<Island> getIslands() { return islands; }
/**
* 获取四邻域格子
*/
public List<Cell> getNeighbors(int r, int c) {
List<Cell> neighbors = new ArrayList<>();
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (isValidCell(nr, nc)) neighbors.add(new Cell(nr, nc));
}
return neighbors;
}
public NurikabeBoard copy() {
NurikabeBoard copy = new NurikabeBoard(numbers);
for (int r = 0; r < rows; r++) {
System.arraycopy(grid[r], 0, copy.grid[r], 0, cols);
}
return copy;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (numbers[r][c] > 0) sb.append(numbers[r][c]);
else sb.append(grid[r][c].getSymbol());
sb.append(' ');
}
sb.append('\n');
}
return sb.toString();
}
}
约束传播引擎
/**
* 约束传播求解器,应用数墙的确定性规则
*/
class ConstraintPropagator {
/**
* 执行一轮约束传播,返回是否有新格子被确定
*/
public boolean propagate(NurikabeBoard board) {
boolean changed = false;
// 规则1: 已完成的岛屿,相邻未知格必为黑
for (Island island : board.getIslands()) {
if (island.isComplete()) {
for (Cell cell : island.cells) {
for (Cell n : board.getNeighbors(cell.row, cell.col)) {
if (board.get(n.row, n.col) == CellState.UNKNOWN) {
board.set(n.row, n.col, CellState.WATER_BLACK);
changed = true;
}
}
}
}
}
// 规则2: 数字格相邻的另一个数字格之间必须涂黑(不同岛屿不能相连)
for (int r = 0; r < board.getRows(); r++) {
for (int c = 0; c < board.getCols(); c++) {
if (board.hasNumber(r, c)) {
for (Cell n : board.getNeighbors(r, c)) {
if (board.hasNumber(n.row, n.col)) {
// 找到两个数字格相邻的中点?不,直接相邻就涂中间?
// 实际上数字格本身已经是白色,只需确保它们不被错误连接
}
}
}
}
}
// 规则3: 避免出现2×2黑格
for (int r = 0; r < board.getRows() - 1; r++) {
for (int c = 0; c < board.getCols() - 1; c++) {
changed |= preventBlack2x2(board, r, c);
}
}
// 规则4: 检查海洋连通性,隔离的海洋格必须扩展
changed |= propagateWaterConnectivity(board);
// 规则5: 岛屿大小约束
changed |= propagateIslandSize(board);
return changed;
}
/**
* 防止2×2全黑:如果某2×2区域已有3个黑格,最后一个未知格必为白
*/
private boolean preventBlack2x2(NurikabeBoard board, int r, int c) {
boolean changed = false;
Cell[] square = {
new Cell(r, c), new Cell(r, c+1),
new Cell(r+1, c), new Cell(r+1, c+1)
};
int blackCount = 0;
Cell unknown = null;
for (Cell cell : square) {
CellState s = board.get(cell.row, cell.col);
if (s == CellState.WATER_BLACK) blackCount++;
else if (s == CellState.UNKNOWN) unknown = cell;
}
if (blackCount == 3 && unknown != null) {
board.set(unknown.row, unknown.col, CellState.ISLAND_WHITE);
changed = true;
}
return changed;
}
/**
* 传播海洋连通性约束:确保所有黑格最终能连通
*/
private boolean propagateWaterConnectivity(NurikabeBoard board) {
boolean changed = false;
// 简化版:如果某个未知格被黑格包围且其周围有孤立黑格,可能需涂黑以连接
// 完整实现需要更复杂的连通分量分析
return changed;
}
/**
* 传播岛屿大小约束:如果某岛屿还差k格才能达标,
* 且恰好有k个相邻未知格可达,则这些格必为白
*/
private boolean propagateIslandSize(NurikabeBoard board) {
boolean changed = false;
for (Island island : board.getIslands()) {
int remaining = island.targetSize - island.currentSize();
if (remaining <= 0) continue;
// BFS找可达的未知格
Set<Cell> reachableUnknown = new HashSet<>();
Queue<Cell> queue = new LinkedList<>(island.cells);
Set<Cell> visited = new HashSet<>(island.cells);
while (!queue.isEmpty()) {
Cell cur = queue.poll();
for (Cell n : board.getNeighbors(cur.row, cur.col)) {
if (visited.contains(n)) continue;
CellState s = board.get(n.row, n.col);
if (s == CellState.UNKNOWN) {
reachableUnknown.add(n);
} else if (s == CellState.ISLAND_WHITE &&
!board.hasNumber(n.row, n.col)) {
// 属于当前岛屿的白格(非数字格)
visited.add(n);
queue.add(n);
}
}
}
// 如果可达未知格恰好等于剩余需求,全部涂白
if (reachableUnknown.size() == remaining) {
for (Cell cell : reachableUnknown) {
board.set(cell.row, cell.col, CellState.ISLAND_WHITE);
island.cells.add(cell);
changed = true;
}
}
}
return changed;
}
/**
* 执行完整约束传播直到不动点
*/
public void propagateFully(NurikabeBoard board) {
while (propagate(board)) {
// 持续传播直到没有新推断
}
}
}
Flood Fill连通性检测
/**
* Flood Fill连通性检测工具
*/
class ConnectivityChecker {
/**
* 检查指定状态的格子是否全部连通(四连通)
*/
public boolean isConnected(NurikabeBoard board, CellState targetState) {
// 找到第一个目标状态格子
Cell start = null;
outer:
for (int r = 0; r < board.getRows(); r++) {
for (int c = 0; c < board.getCols(); c++) {
if (board.get(r, c) == targetState ||
(targetState == CellState.WATER_BLACK && board.get(r, c) == CellState.UNKNOWN)) {
start = new Cell(r, c);
break outer;
}
}
}
if (start == null) return true; // 没有该状态格子
// BFS Flood Fill
Set<Cell> visited = new HashSet<>();
Queue<Cell> queue = new LinkedList<>();
queue.add(start);
visited.add(start);
while (!queue.isEmpty()) {
Cell cur = queue.poll();
for (Cell n : board.getNeighbors(cur.row, cur.col)) {
if (visited.contains(n)) continue;
CellState s = board.get(n.row, n.col);
// 对黑格连通性检查,未知格也可能是黑
boolean match = (s == targetState) ||
(targetState == CellState.WATER_BLACK && s == CellState.UNKNOWN);
if (match) {
visited.add(n);
queue.add(n);
}
}
}
// 检查是否所有目标状态格子都被访问
for (int r = 0; r < board.getRows(); r++) {
for (int c = 0; c < board.getCols(); c++) {
if (board.get(r, c) == targetState && !visited.contains(new Cell(r, c))) {
return false;
}
}
}
return true;
}
/**
* 检查是否存在2×2全黑区域
*/
public boolean hasBlack2x2(NurikabeBoard board) {
for (int r = 0; r < board.getRows() - 1; r++) {
for (int c = 0; c < board.getCols() - 1; c++) {
if (isBlack(board, r, c) && isBlack(board, r, c+1) &&
isBlack(board, r+1, c) && isBlack(board, r+1, c+1)) {
return true;
}
}
}
return false;
}
private boolean isBlack(NurikabeBoard board, int r, int c) {
return board.get(r, c) == CellState.WATER_BLACK;
}
/**
* 检查所有岛屿是否满足大小约束且不连通
*/
public boolean isValidIslands(NurikabeBoard board, List<Island> islands) {
// 对每个已完成的岛屿,做Flood Fill验证大小
for (Island island : islands) {
if (!island.isComplete()) continue;
Set<Cell> visited = new HashSet<>();
Queue<Cell> queue = new LinkedList<>();
queue.add(island.seed);
visited.add(island.seed);
while (!queue.isEmpty()) {
Cell cur = queue.poll();
for (Cell n : board.getNeighbors(cur.row, cur.col)) {
if (visited.contains(n)) continue;
CellState s = board.get(n.row, n.col);
// 白格且不属于其他数字格
if (s == CellState.ISLAND_WHITE) {
// 检查是否属于其他岛屿
boolean otherIsland = false;
for (Island other : islands) {
if (other != island && other.seed.equals(n)) {
otherIsland = true;
break;
}
}
if (!otherIsland) {
visited.add(n);
queue.add(n);
}
}
}
}
if (visited.size() != island.targetSize) return false;
}
return true;
}
}
回溯搜索求解器
/**
* 数墙回溯搜索求解器
*/
class NurikabeSolver {
private final ConstraintPropagator propagator = new ConstraintPropagator();
private final ConnectivityChecker checker = new ConnectivityChecker();
private int steps = 0;
/**
* 求解数墙谜题
*/
public NurikabeBoard solve(NurikabeBoard board) {
steps = 0;
// 先进行约束传播简化问题
propagator.propagateFully(board);
if (solveRecursive(board)) {
return board;
}
return null;
}
private boolean solveRecursive(NurikabeBoard board) {
steps++;
// 检查当前状态是否违反约束
if (checker.hasBlack2x2(board)) return false;
if (!checker.isConnected(board, CellState.WATER_BLACK)) {
// 未完成的黑格不连通可能是正常的,需要更精细判断
}
// 检查是否全部确定
if (isComplete(board)) {
return validateSolution(board);
}
// 选择启发式最优的未知格进行分支
Cell bestCell = selectCell(board);
if (bestCell == null) return validateSolution(board);
// 尝试设为白色(岛屿)
NurikabeBoard copyWhite = board.copy();
copyWhite.set(bestCell.row, bestCell.col, CellState.ISLAND_WHITE);
propagator.propagateFully(copyWhite);
if (solveRecursive(copyWhite)) {
// 将解复制回原board
copyGrid(copyWhite, board);
return true;
}
// 尝试设为黑色(海洋)
NurikabeBoard copyBlack = board.copy();
copyBlack.set(bestCell.row, bestBlack.col, CellState.WATER_BLACK);
propagator.propagateFully(copyBlack);
if (solveRecursive(copyBlack)) {
copyGrid(copyBlack, board);
return true;
}
return false;
}
private Cell selectCell(NurikabeBoard board) {
// 优先选择约束最强的格子(最少剩余值启发式)
Cell best = null;
int minOptions = 3;
for (int r = 0; r < board.getRows(); r++) {
for (int c = 0; c < board.getCols(); c++) {
if (board.get(r, c) == CellState.UNKNOWN) {
int options = countValidOptions(board, r, c);
if (options < minOptions) {
minOptions = options;
best = new Cell(r, c);
}
}
}
}
return best;
}
private int countValidOptions(NurikabeBoard board, int r, int c) {
int count = 0;
// 快速检查设为白/黑是否会立即违规
if (!wouldViolate2x2(board, r, c, CellState.WATER_BLACK)) count++;
if (!wouldIsolateIsland(board, r, c)) count++;
return count;
}
private boolean wouldViolate2x2(NurikabeBoard board, int r, int c, CellState state) {
if (state != CellState.WATER_BLACK) return false;
// 临时设为黑,检查是否形成2x2
// 简化检查
return false;
}
private boolean wouldIsolateIsland(NurikabeBoard board, int r, int c) {
// 如果某数字格的所有相邻未知格被阻断,则无法扩展
return false;
}
private boolean isComplete(NurikabeBoard board) {
for (int r = 0; r < board.getRows(); r++) {
for (int c = 0; c < board.getCols(); c++) {
if (board.get(r, c) == CellState.UNKNOWN) return false;
}
}
return true;
}
private boolean validateSolution(NurikabeBoard board) {
// 检查所有约束
if (checker.hasBlack2x2(board)) return false;
if (!checker.isConnected(board, CellState.WATER_BLACK)) return false;
// 检查每个岛屿大小
for (Island island : board.getIslands()) {
Set<Cell> visited = new HashSet<>();
Queue<Cell> queue = new LinkedList<>();
queue.add(island.seed);
visited.add(island.seed);
while (!queue.isEmpty()) {
Cell cur = queue.poll();
for (Cell n : board.getNeighbors(cur.row, cur.col)) {
if (visited.contains(n)) continue;
if (board.get(n.row, n.col) == CellState.ISLAND_WHITE) {
visited.add(n);
queue.add(n);
}
}
}
if (visited.size() != island.targetSize) return false;
}
return true;
}
private void copyGrid(NurikabeBoard from, NurikabeBoard to) {
for (int r = 0; r < from.getRows(); r++) {
for (int c = 0; c < from.getCols(); c++) {
to.set(r, c, from.get(r, c));
}
}
}
public int getSteps() { return steps; }
}
主程序与测试
public class NurikabeMain {
public static void main(String[] args) {
// 5×5 简单谜题示例
// 数字表示岛屿大小,0表示空白
int[][] puzzle = {
{5, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 3}
};
System.out.println("=== 数墙谜题 ===");
NurikabeBoard board = new NurikabeBoard(puzzle);
System.out.println(board);
System.out.println("=== 开始求解 ===");
NurikabeSolver solver = new NurikabeSolver();
NurikabeBoard solution = solver.solve(board);
if (solution != null) {
System.out.println("求解成功!搜索步数: " + solver.getSteps());
System.out.println(solution);
} else {
System.out.println("求解失败");
}
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 约束传播 | O(R×C) | O(R×C) |
| Flood Fill | O(R×C) | O(R×C) |
| 2×2检测 | O(R×C) | O(1) |
| 回溯搜索(最坏) | O(2^(R×C)) | O(R×C×深度) |
| 实际带约束传播 | 远小于最坏情况 | O(R×C) |
总结
数墙求解器融合了多种经典算法:
- 约束传播:利用数墙的确定性规则(如2×2禁止、岛屿大小限制)快速推断,减少搜索空间
- Flood Fill:基于BFS的连通性检测,验证海洋连通和岛屿大小约束
- 回溯搜索:对不确定的格子进行分支尝试,配合启发式选点(MRV)提高效率
- 复制-恢复机制:通过棋盘深拷贝实现搜索状态的高效回溯
约束传播是求解数墙的关键——在典型谜题中,70%以上的格子可以通过纯逻辑推理确定,无需回溯。