数织游戏也叫日本拼图,玩家需要在矩阵网格中,依据数字提示填充单元格。
左侧的数字代表每一行连续单元格的数量,顶上的数字代表每一列连续单元格的数量。
最终的填充结果必须同时满足行和列的限制,才算完成拼图。
- 当第一次看到这个问题时,直觉告诉我需要用到回溯算法求解。回溯算法就是尝试搜索每一种可能性,一旦遇到不符合要求的情况,立即停止继续搜索并返回之前的状态。通常回溯需要配合递归使用,这样可以精简代码结构优化执行速度。
- 不过对于这种行和列都有限制的问题,该如何设计回溯算法呢。我的想法是先处理行,根据行规则的限制,排列组合每一行内相应数量的连续方块,得到每一行的可能排列形式,再将这些合法的行互相组合,最终筛选出符合列规则要求的结果。
- 上面的方法用到了两次递归回溯,一次处理行内连续方块的排列情况,一次处理列内各种行的排列情况。由于记录这些排列情况需要占用大量内存,导致算法效率不够理想,无论我如何剪枝优化,始终难以求解 40 层以上的数织。
- 既然单元格只有填充和留空两种状态,那不是可以转换成01背包问题求解。由于求解过程中会出现大量重叠状态,所以得利用递归的特性来避免重复计算,除此以外状态的枚举会随着矩阵层数呈指数上升,不进行剪枝的话,时间复杂度会达到惊人的 O(2^(m*n))。
- 剪枝的意义在于筛选掉错误的搜索路径,大幅优化搜索过程中的执行效率。这需要对中间状态进行判断,不过在此之前先要确定有哪些状态。这个问题中连续方块的序号、填充的数量、空白的数量,这些是需要被记录的。在进入下一层递归前,逻辑运算会判断当前状态是否合法,只有在填充和留空操作后,仍然符合规则要求的状态,才会继续进行接下来的搜索。
- 依照上面的思路,最终得到如下代码。求解 40 层只需 50 毫秒,50 层也不超过 12 秒。
package T007_解数织;
/**
* 数织是一种逻辑解谜游戏,它的规则简单,解题过程富有挑战性。
* 游戏规则很简单。
* 游戏棋盘是一张正方形网格,其中的每个格子最终需要涂成黑色或标记为X。
* 棋盘每一行左边或每一列上方的数字表示该行或该列上每一组相邻的黑色方格的长度。
* 游戏目标是要找出所有的黑色方格。
*/
class Solution {
private int[][] graph; // 数图
private int width; // 数图宽度
private int height; // 数图高度
private int[][] result; // 图迷结果
private int[][] rowLimit; // 行限制
private int[][] colLimit; // 列限制
private int[] rowPoint; // 行限制指针
private int[] colPoint; // 列限制指针
private int[] rowCount; // 行填充计数
private int[] colCount; // 列填充计数
private int[] rowSpace; // 行剩余空格
private int[] colSpace; // 列剩余空格
public int[][] solveKatana(int[][] rowLimit, int[][] colLimit) {
this.result = null;
this.rowLimit = rowLimit;
this.colLimit = colLimit;
this.height = rowLimit.length;
this.width = colLimit.length;
this.graph = new int[height][width];
this.rowPoint = new int[height];
this.colPoint = new int[width];
this.rowCount = new int[height];
this.colCount = new int[width];
this.rowSpace = deal(rowLimit, width);
this.colSpace = deal(colLimit, height);
dfs(0, 0);
return result;
}
// 计算行列空格数量
private int[] deal(int[][] limit, int total) {
int len = limit.length;
int[] space = new int[len];
for (int i = 0; i < len; i++) {
space[i] = total;
for (int num : limit[i]) {
space[i] -= num;
}
}
return space;
}
// 递归回溯算法
private void dfs(int i, int j) {
if (result != null) {
return;
}
// 结束
if (i == height) {
result = graph;
graph = new int[height][width];
return;
}
// 换行
if (j == width) {
dfs(i + 1, 0);
return;
}
// 填充
boolean rowFit = (rowPoint[i] < rowLimit[i].length && rowCount[i] < rowLimit[i][rowPoint[i]]);
boolean colFit = (colPoint[j] < colLimit[j].length && colCount[j] < colLimit[j][colPoint[j]]);
if (rowFit && colFit) {
boolean rowEnd = (j == width - 1) && (rowCount[i] + 1 == rowLimit[i][rowPoint[i]]);
boolean colEnd = (i == height - 1) && (colCount[j] + 1 == colLimit[j][colPoint[j]]);
boolean notEnd = (i < height - 1) && (j < width - 1);
if (rowEnd || colEnd || notEnd) {
this.fill(i, j);
}
}
// 跳过
rowFit = (rowCount[i] == 0) || (rowPoint[i] < rowLimit[i].length && rowCount[i] == rowLimit[i][rowPoint[i]]);
colFit = (colCount[j] == 0) || (colPoint[j] < colLimit[j].length && colCount[j] == colLimit[j][colPoint[j]]);
if (rowFit && colFit) {
boolean rowEnd = (j == width - 1) && (rowSpace[i] - 1 == 0);
boolean colEnd = (i == height - 1) && (colSpace[j] - 1 == 0);
boolean notEnd = (i < height - 1) && (j < width - 1) && (rowSpace[i] > 0) && (colSpace[j] > 0);
if (rowEnd || colEnd || notEnd) {
this.skip(i, j);
}
}
}
// 填充当前方格
private void fill(int i, int j) {
{
graph[i][j] = 1;
rowCount[i] += 1;
colCount[j] += 1;
}
dfs(i, j + 1);
{
graph[i][j] = 0;
rowCount[i] -= 1;
colCount[j] -= 1;
}
}
// 跳过当前方格
private void skip(int i, int j) {
int rowCountTemp = rowCount[i];
int colCountTemp = colCount[j];
int rowPointTemp = rowPoint[i];
int colPointTemp = colPoint[j];
{
if (rowCount[i] != 0) {
rowPoint[i] += 1;
}
if (colCount[j] != 0) {
colPoint[j] += 1;
}
rowCount[i] = 0;
colCount[j] = 0;
rowSpace[i] -= 1;
colSpace[j] -= 1;
}
dfs(i, j + 1);
{
rowCount[i] = rowCountTemp;
colCount[j] = colCountTemp;
rowPoint[i] = rowPointTemp;
colPoint[j] = colPointTemp;
rowSpace[i] += 1;
colSpace[j] += 1;
}
}
}
public class TestDemo {
/**
* 输入:
* rowLimit = [[1,1],[5],[1,1,1],[5],[1]]
* colLimit = [[4],[1,2],[3],[2,1],[3]]
* 输出:
* [1, 0, 0, 1, 0]
* [1, 1, 1, 1, 1]
* [1, 0, 1, 0, 1]
* [1, 1, 1, 1, 1]
* [0, 1, 0, 0, 0]
*/
public static void main(String[] args) {
int[][] rowLimit = {{1, 1}, {5}, {1, 1, 1}, {5}, {1}};
int[][] colLimit = {{4}, {1, 2}, {3}, {2, 1}, {3}};
Solution solution = new Solution();
long t1 = System.currentTimeMillis();
int[][] graph = solution.solveKatana(rowLimit, colLimit);
long t2 = System.currentTimeMillis();
System.out.println(TestDemo.toString(graph));
System.out.println("solution.solveKatana " + (t2 - t1) + " ms");
}
// 字符串形式表示二维图形
public static String toString(int[][] graph) {
if (graph == null) {
return null;
}
final String NONE = "0";
final String FILL = "1";
StringBuilder sb = new StringBuilder();
for (int[] row : graph) {
sb.append("[");
for (int j = 0; j < row.length; j++) {
if (row[j] == 0) {
sb.append(NONE);
} else if (row[j] == 1) {
sb.append(FILL);
}
if (j != row.length - 1) {
sb.append(", ");
}
}
sb.append("]");
sb.append("\n");
}
return sb.toString();
}
}