`
cgs1999
  • 浏览: 537311 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

用Java实现N*N的标准数独及对角线数独解题

阅读更多
1、引言
前一段时间迷上了数独游戏(不知道数独的同学请自行百度,或点这里了解),就有了想编程实现解题的想法。一直拖到最近,终于抽空使用Java实现了3*3标准数独的解题,并在其基础上抽象出了N*N的标准数独及对角线数独的解题。现和众位分享相关的代码和过程。

特别说明:这里的N*N标准数独,指的是N=n*n(n为正整数),即4*4、9*9、16*16、25*25……(n=1没意义)

2、解题思路
数独的解题方法有很多种,有兴趣的同学可以自行百度或点这里了解

我使用的是最简单的,也是比较容易实现的基础摒除法。
在每个空格上,都递归尝试1~N的每个值的可能情况,校验每个值是否符合基础摒除法,不符合则尝试下一个,直至尝试N个值结束。
引用

基础摒除法就是利用1~N的值在每一行、每一列、每一个N宫格都只能出现一次的规则进行解题的方法。基础摒除法可以分为行摒除、列摒除、N宫格摒除。


3、实现过程
3.1 几个定义
1、N宫格中,“空格”的定义,采用空字符串("")或0表示;

2、N宫格中,行或列的索引定义,为编码方便采用0~(N-1)来表示,如:第4行第5列的格子对应的行列索引为:row=3和col=4;

3、N宫格中,填空内容的定义,采用长度为N的一维数组表示,如:
n=2,即N=4
// 4*4填空内容
String[] dataArray = new String[] {
	"1", "2", "3", "4"
});

n=3,即N=9
// 9*9填空内容
String[] dataArray = new String[] {
	"1", "2", "3", "4", "5", "6", "7", "8", "9"
});

n=4,即N=16
// 16*16填空内容
String[] dataArray = new String[] {
	"1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G"
});


当然,N宫格的填空内容,不局限于1~N的数值,你可以使用任何非“空格”的且互不一样的字符串,如:
// 4*4填空内容
String[] dataArray = new String[] {
	"张三", "李四", "王五", "赵六"
});


4、N宫格中,N*N的数独题目或填空结果定义,采用长度为N*N的二维数组表示,如:
// 9*9数独题目
String[][] resultArray = new String[][] {
	{
		"0", "2", "0", "0", "0", "0", "0", "0", "0"
	}, {
		"5", "0", "6", "0", "0", "0", "3", "0", "9"
	}, {
		"0", "8", "0", "5", "0", "2", "0", "6", "0"
	}, {
		"0", "0", "5", "0", "7", "0", "1", "0", "0"
	}, {
		"0", "0", "0", "2", "0", "8", "0", "0", "0"
	}, {
		"0", "0", "4", "0", "1", "0", "8", "0", "0"
	}, {
		"0", "5", "0", "8", "0", "7", "0", "3", "0"
	}, {
		"7", "0", "2", "0", "0", "0", "4", "0", "5"
	}, {
		"0", "4", "0", "0", "0", "0", "0", "7", "0"
	}
};


5、N宫格中,每个格子的索引定义,采用从0开始的整数来表示,那么N*N格子索引范围为:0~(N*N-1),如:9*9的索引范围为0~80,那么,格子索引和行列索引可以进行相关的换算;

3.2 基础摒除法的Java实现
1、行摒除
/**
 * 行校验
 * @param resultArray
 * @param row
 * @param value
 * @return
 */
private static boolean checkRow(final String[][] resultArray, int row, String value) {
	int arrayLen = resultArray.length;
	for (int i = 0; i < arrayLen; i++) {
		if (value.equals(resultArray[row][i])) {
			return false;
		}
	}
	return true;
}


2、列摒除
/**
 * 列校验
 * @param resultArray
 * @param col
 * @param value
 * @return
 */
private static boolean checkColumn(final String[][] resultArray, int col, String value) {
	int arrayLen = resultArray.length;
	for (int i = 0; i < arrayLen; i++) {
		if (value.equals(resultArray[i][col])) {
			return false;
		}
	}
	return true;
}


3、N宫摒除
这里实现比较难的一点在于,根据给定行、列计算其所在宫的行列开始值
/**
 * 宫校验
 * @param resultArray
 * @param row
 * @param col
 * @param value
 * @return
 */
private static boolean checkBlock(final String[][] resultArray, int row, int col, String value) {
	int arrayLen = resultArray.length;
	int blockLen = (int) Math.sqrt(arrayLen);
	int blockRowIndex = (int) row / blockLen;
	int blockColIndex = (int) col / blockLen;
	int blockRowStart = blockLen * blockRowIndex;
	int blockColStart = blockLen * blockColIndex;

	for (int i = 0; i < blockLen; i++) {
		int rowIndex = blockRowStart + i;
		for (int j = 0; j < blockLen; j++) {
			int colIndex = blockColStart + j;
			if (value.equals(resultArray[rowIndex][colIndex])) {
				return false;
			}
		}
	}
	return true;
}


4、对角线摒除(左上至右下)
/**
 * 对角线校验(左上至右下)
 * @param resultArray
 * @param value
 * @return
 */
private static boolean checkLeftTop2RightBottom(final String[][] resultArray, int row, int col, String value) {
	if (row == col) {
		int arrayLen = resultArray.length;
		for (int i = 0; i < arrayLen; i++) {
			if (value.equals(resultArray[i][i])) {
				return false;
			}
		}
	}
	return true;
}


5、对角线摒除(左下至右上)
/**
 * 对角线校验(左下至右上)
 * @param resultArray
 * @param value
 * @return
 */
private static boolean checkLeftBottom2RightTop(final String[][] resultArray, int row, int col, String value) {
	int arrayLen = resultArray.length;
	if ((row + col) == (arrayLen - 1)) {
		for (int i = 0; i < arrayLen; i++) {
			if (value.equals(resultArray[arrayLen - i - 1][i])) {
				return false;
			}
		}
	}
	return true;
}


6、基础摒除法
/**
 * 执行所有校验
 * @param resultArray
 * @param row
 * @param col
 * @param value
 * @param checkCross
 * @return
 */
private static boolean checkAll(final String[][] resultArray, int row, int col, String value, boolean checkCross) {
	// 行校验
	if (!checkRow(resultArray, row, value)) {
		return false;
	}

	// 列校验
	if (!checkColumn(resultArray, col, value)) {
		return false;
	}

	// 宫校验
	if (!checkBlock(resultArray, row, col, value)) {
		return false;
	}

	// 对角线校验
	if (checkCross) {
		// 对角线校验(左上至右下)
		if (!checkLeftTop2RightBottom(resultArray, row, col, value)) {
			return false;
		}
		// 对角线校验(左下至右上)
		if (!checkLeftBottom2RightTop(resultArray, row, col, value)) {
			return false;
		}
	}

	return true;
}


3.3 解题实现
解题采用递归的方式进行,直至解题完成并打印出结果,相关实现代码如下
1、校验是否已填好的实现如下:
/**
 * 校验是否已经填好
 * @param value
 * @return
 */
private static boolean isUnselect(String value) {
	return "".equals(value) || "0".equals(value);
}


2、递归过程的填空结果传递,需要复制二维数组,其实现如下:
/**
 * 复制数组
 * @param array
 * @return
 */
private static String[][] copyArray(final String[][] array) {
	int rowCount = array.length;
	int colCount = array[0].length;
	String[][] copy = new String[rowCount][colCount];
	for (int i = 0; i < rowCount; i++) {
		for (int j = 0; j < colCount; j++) {
			copy[i][j] = array[i][j];
		}
	}
	return copy;
}


3、打印解题结果的实现代码如下:
/**
 * 输出结果
 * @param resultArray
 */
private static void printResult(final String[][] resultArray) {
	System.out.println("\n--------------------------------");
	int arrayLen = resultArray.length;
	for (int i = 0; i < arrayLen; i++) {
		System.out.println(Arrays.asList(resultArray[i]));
	}
}


4、递归解题算法(核心)
/**
 * 数独解题
 * @param dataArray 待选列表
 * @param resultArray 前面(resultIndex-1)个的填空结果
 * @param resultIndex 选择索引,从0开始
 * @param checkCross 是否是对角线数独
 */
private static void sudoSelect(String[] dataArray, final String[][] resultArray, int resultIndex, boolean checkCross) {
	int resultLen = resultArray.length;
	if (resultIndex >= (int) Math.pow(resultLen, 2)) {
		// 全部填完时,输出排列结果
		printResult(resultArray);
		return;
	}

	int row = (int) resultIndex / resultLen;
	int col = resultIndex % resultLen;
	if (isUnselect(resultArray[row][col])) {
		// 逐个尝试,递归选择下一个
		for (int i = 0; i < dataArray.length; i++) {
			if (checkAll(resultArray, row, col, dataArray[i], checkCross)) {
				// 排列结果不存在该项,才可选择
				String[][] resultCopy = copyArray(resultArray);

				resultCopy[row][col] = dataArray[i];
				sudoSelect(dataArray, resultCopy, resultIndex + 1, checkCross);
			}
		}
	} else {
		// 递归选择下一个
		String[][] resultCopy = copyArray(resultArray);
		sudoSelect(dataArray, resultCopy, resultIndex + 1, checkCross);
	}
}


3.4 其它
1、根据待选数组,初始化生成二维结果数组;
/**
 * 初始化结果数组
 * @param dataArray 待选列表
 * @return
 */
public static String[][] initResultArray(String[] dataArray) {
	int arrayLen = dataArray.length;
	String[][] resultArray = new String[arrayLen][arrayLen];
	for (int i = 0; i < arrayLen; i++) {
		for (int j = 0; j < arrayLen; j++) {
			resultArray[i][j] = "0";
		}
	}
	return resultArray;
}


2、根据N*N长度的字符串,初始化生成二维结果数组;
/**
 * 初始化结果数组
 * @param resultString 结果字符串
 * @return
 */
public static String[][] initResultArray(String resultString) {
	int arrayLen = (int) Math.sqrt(resultString.length());
	String[][] resultArray = new String[arrayLen][arrayLen];
	for (int i = 0; i < arrayLen; i++) {
		for (int j = 0; j < arrayLen; j++) {
			resultArray[i][j] = "" + resultString.charAt(i * arrayLen + j);
		}
	}
	return resultArray;
}


4、测试
4.1 测试代码
1、为测试方便,进行了几个封装
/**
 * 9*9数独给定已选字符串求解
 * @param resultString 数独题目
 */
public static void sudoSelect(String resultString) {
	String[][] resultArray = initResultArray(resultString);
	sudoSelect(new String[] {
			"1", "2", "3", "4", "5", "6", "7", "8", "9"
	}, resultArray);
}

/**
 * N*N数独给定结果数组求解
 * @param dataArray 待选列表
 * @param resultArray 已选结果数组
 */
public static void sudoSelect(String[] dataArray, final String[][] resultArray) {
	sudoSelect(dataArray, resultArray, false);
}

/**
 * 排列选择(从列表中选择n个排列)
 * @param dataArray 待选列表
 * @param resultArray 已选结果
 * @param checkCross 是否校验对角线
 */
public static void sudoSelect(String[] dataArray, final String[][] resultArray, boolean checkCross) {
	sudoSelect(dataArray, resultArray, 0, checkCross);
}


2、测试入口
public static void main(String[] args) {
	// 求解给定数独所有可能
	sudoSelect(new String[] {
			"1", "2", "3", "4", "5", "6", "7", "8", "9"
	}, new String[][] {
			{
					"9", "1", "2", "0", "0", "7", "0", "5", "0"
			}, {
					"0", "0", "3", "0", "5", "9", "0", "2", "1"
			}, {
					"0", "0", "5", "4", "1", "2", "0", "0", "9"
			}, {
					"0", "8", "0", "0", "4", "5", "9", "0", "2"
			}, {
					"0", "0", "0", "0", "7", "0", "5", "0", "0"
			}, {
					"5", "0", "4", "0", "6", "0", "0", "1", "0"
			}, {
					"0", "0", "0", "5", "0", "6", "0", "0", "0"
			}, {
					"2", "5", "0", "7", "0", "0", "8", "0", "0"
			}, {
					"0", "3", "0", "0", "0", "0", "0", "9", "5"
			}
	});

	// 求解给定数独所有可能
	// http://tieba.baidu.com/p/4813549830
	// #9806 002300609000000075100060000504100008060050040800007102000030001250000000907004200
	// #9807 010000000000294000008300709180002040050000080030800096401003800000471000000000020
	// #9808 100200905000080000400600023010005060000060000050400030840001007000070000507002001
	// #9809 300500090400000500002310000053080010000090000060050370000021800001000004080007006
	// #9810 010500000090073000804020000400000100780060029002000005000030207000480060000006090
	sudoSelect("002300609000000075100060000504100008060050040800007102000030001250000000907004200");
	sudoSelect("010000000000294000008300709180002040050000080030800096401003800000471000000000020");
	sudoSelect("100200905000080000400600023010005060000060000050400030840001007000070000507002001");
	sudoSelect("300500090400000500002310000053080010000090000060050370000021800001000004080007006");
	sudoSelect("010500000090073000804020000400000100780060029002000005000030207000480060000006090");
}


4.2 测试结果
1、运行测试代码,控制台输出结果如下:
--------------------------------
[9, 1, 2, 6, 3, 7, 4, 5, 8]
[6, 4, 3, 8, 5, 9, 7, 2, 1]
[8, 7, 5, 4, 1, 2, 6, 3, 9]
[1, 8, 7, 3, 4, 5, 9, 6, 2]
[3, 6, 9, 2, 7, 1, 5, 8, 4]
[5, 2, 4, 9, 6, 8, 3, 1, 7]
[4, 9, 8, 5, 2, 6, 1, 7, 3]
[2, 5, 1, 7, 9, 3, 8, 4, 6]
[7, 3, 6, 1, 8, 4, 2, 9, 5]
--------------------------------
[4, 8, 2, 3, 7, 5, 6, 1, 9]
[3, 9, 6, 4, 2, 1, 8, 7, 5]
[1, 7, 5, 8, 6, 9, 3, 2, 4]
[5, 2, 4, 1, 9, 3, 7, 6, 8]
[7, 6, 1, 2, 5, 8, 9, 4, 3]
[8, 3, 9, 6, 4, 7, 1, 5, 2]
[6, 4, 8, 7, 3, 2, 5, 9, 1]
[2, 5, 3, 9, 1, 6, 4, 8, 7]
[9, 1, 7, 5, 8, 4, 2, 3, 6]
--------------------------------
[6, 1, 9, 7, 5, 8, 2, 3, 4]
[5, 7, 3, 2, 9, 4, 6, 1, 8]
[2, 4, 8, 3, 1, 6, 7, 5, 9]
[1, 8, 6, 9, 3, 2, 5, 4, 7]
[9, 5, 4, 1, 6, 7, 3, 8, 2]
[7, 3, 2, 8, 4, 5, 1, 9, 6]
[4, 9, 1, 6, 2, 3, 8, 7, 5]
[8, 2, 5, 4, 7, 1, 9, 6, 3]
[3, 6, 7, 5, 8, 9, 4, 2, 1]
--------------------------------
[1, 7, 6, 2, 3, 4, 9, 8, 5]
[3, 2, 5, 1, 8, 9, 4, 7, 6]
[4, 9, 8, 6, 5, 7, 1, 2, 3]
[7, 1, 3, 9, 2, 5, 8, 6, 4]
[2, 8, 4, 7, 6, 3, 5, 1, 9]
[6, 5, 9, 4, 1, 8, 7, 3, 2]
[8, 4, 2, 3, 9, 1, 6, 5, 7]
[9, 3, 1, 5, 7, 6, 2, 4, 8]
[5, 6, 7, 8, 4, 2, 3, 9, 1]
--------------------------------
[3, 7, 6, 5, 4, 8, 2, 9, 1]
[4, 1, 8, 2, 7, 9, 5, 6, 3]
[5, 9, 2, 3, 1, 6, 7, 4, 8]
[9, 5, 3, 7, 8, 4, 6, 1, 2]
[1, 2, 7, 6, 9, 3, 4, 8, 5]
[8, 6, 4, 1, 5, 2, 3, 7, 9]
[6, 4, 5, 9, 2, 1, 8, 3, 7]
[7, 3, 1, 8, 6, 5, 9, 2, 4]
[2, 8, 9, 4, 3, 7, 1, 5, 6]
--------------------------------
[3, 1, 6, 5, 4, 8, 9, 7, 2]
[2, 9, 5, 1, 7, 3, 6, 4, 8]
[8, 7, 4, 6, 2, 9, 5, 1, 3]
[4, 5, 3, 7, 9, 2, 1, 8, 6]
[7, 8, 1, 3, 6, 5, 4, 2, 9]
[9, 6, 2, 8, 1, 4, 7, 3, 5]
[6, 4, 8, 9, 3, 1, 2, 5, 7]
[5, 2, 9, 4, 8, 7, 3, 6, 1]
[1, 3, 7, 2, 5, 6, 8, 9, 4]


2、经校验结果正确。

5、思考
1、给定的数独题目可能不止一个解,本方法可以给出所有的可能结果,极限情况:当给定的是一个空的二维数组时,则可以输出N*N的所有终盘组合;

如:可以通过下面的测试代码,得到4*4数独的所有终盘组合,其数量为288
String[] dataArray = new String[] {
	"1", "2", "3", "4"
};
String[][] resultArray = initResultArray(dataArray);
sudoSelect(dataArray, resultArray);


9*9数独的所有终盘组合,百度贴吧给出的终盘数量为6,670,903,752,021,072,936,960(约为6.67×10的21次方)种组合,有兴趣的童靴可以使用下面的测试代码进行测试(反正我运行了好久都没运行完
String[] dataArray = new String[] {
	"1", "2", "3", "4", "5", "6", "7", "8", "9"
};
String[][] resultArray = initResultArray(dataArray);
sudoSelect(dataArray, resultArray);


2、算法的时间复杂度比较大,可以在递归尝试1~N时,跳过行、列、宫中已存在的数值,后续可以优化,当然也可以采用其它的解题方法,但代码实现相对就会比较复杂了;

3、在相关实现代码的基础上,实现数独生成器也就不难了,具体思路可以如下:
(1)随机生成行、列的索引,这就形成了随机格子,然后判断格子是否为“空格”,若已填则重新随机直至格子为“空格”;
(2)生成随机值填充(1)中出现的随机格子,通过基础摒除法校验,直至随机的值可用,若一直不可用,则进入(1)重新生成;
(3)循环生成并填充不定数量的格子(一般建议是23—30个),然后尝试解题,若无解则进入(1)重新生成;
(4)数独题目生成了,可根据题目已填充格子的数量以及解的数量进行划分难度;

上述的过程中,可能经常出现生成的题目无解,效率比较低,但该方法完全随机且保证了有解。
网上有其它的生成策略,比如:采用挖空法,即对已知的终盘进行挖空不定数量的随机行列格子,另外,可以结合宫内行列调整和宫外行列的调换来实现。


6、源代码
完整源代码点击这里下载
分享到:
评论

相关推荐

    数独(九*九、六*六、七*七)解标准局、对角局、不规则局、杀手局源程序

    - **对角局**:除了行和列的要求外,数独的主对角线和副对角线上的数字也需满足1到9的全集。 - **不规则局**:谜题中的小宫格形状不一定是传统的3x3,可以是各种不规则形状,增加了解题的复杂度。 - **杀手局**:...

    数独技巧完全手册下载

    - **对角线数独**:某些数独变体考虑主对角线和副对角线上的数字,遵循同样不重复的规则。 - **区块对角线**:在一些特殊设计的数独中,除了标准的9个小九宫格外,还可能包含额外的对角线区域,需要考虑这些区域的...

    对角线数独.pdf

    对角线数独作为一种新颖的数独变种,它不仅包含了标准数独所有的规则,还在传统9x9的数独棋盘上增加了两条对角线的限制条件,使得游戏更具挑战性和趣味性。在每一行、每一列以及每个3x3的小宫格内,玩家需要填入1至9...

    想要数独的压缩文件包么?

    - **区块对角线**:高级数独中可能出现的限制条件,数字在两个对角线上也不能重复。 3. **进阶技巧**: - **X-wing, swordfish, jellyfish** 等高级技术,用于解决更复杂的数独谜题,这些技巧涉及到多个宫格的...

    数独小游戏.zip

    4. **对角线策略**:有些数独变体还考虑了主对角线和副对角线上的数字分布,使得游戏更具挑战性。 5. **标记法**:在解题过程中,可以用标记(如候选数)来帮助跟踪可能的数字位置,避免错误。 6. **试错法**:当...

    初级数独题目题后附答案.doc

    - **对角线规则**:虽然标准数独不考虑对角线,但在某些变体中,数字需同时满足行、列及对角线的不重复规则。 4. **完成时间** - 文档中的“完成时间”栏可能是供玩家记录自己解题所用的时间,以评估自己的解题...

    WPF数独游戏

    数独解题算法通常采用基于规则的策略,如单元格检查、区块检查、对角线检查等。其中,最常用的算法是“隐含唯一数”(aka naked single)和“隐藏唯一数”(aka hidden single)。这些算法通过分析已知数字,确定...

    对角线杀手7宫数独题-4页.pdf

    对角线杀手7宫数独题型是一种在传统数独基础上加入了对角线限制条件的变种。对于数独爱好者来说,它不仅仅是对现有解题技巧的挑战,更是对逻辑思维与空间想象能力的深度考验。本篇文章将深入探讨对角线杀手7宫数独题...

    数独计算器

    2. **新游戏生成**:随机生成合法的数独题目,可以有不同难度等级,如标准数独、对角线数独、不规则数独等。 3. **解题算法**:核心功能是自动求解数独,这通常涉及到回溯法或分支剪枝法的实现。 4. **错误检查**:...

    数独游戏解题程序送源码

    在分析和实现数独解题程序时,还需要考虑以下关键点: - 初始化:如何生成合法的数独题目,包括随机生成和读取预设的题目。 - 数据结构:如何表示和存储数独棋盘状态,通常可以使用二维数组或特殊的矩阵结构。 - ...

    数独求解,满足学习娱乐需要

    4. **对角线策略**:虽然标准数独不考虑对角线,但有些变体数独会要求对角线上的数字也遵循规则,这时需要扩展策略来考虑对角线的数字分布。 5. **XY-Wing**、**XYZ-Wing** 和其他高级技巧:这些是更复杂的逻辑推理...

    数独游戏世界杯的题目

    "数独世界杯题目.pdf"这份压缩包文件,很可能是历届数独世界杯的竞赛题目集锦,其中包含了各种类型的数独题目,如标准数独、对角线数独、不规则数独等。每种类型的题目都有其独特的解题策略和技巧: 1. **标准数独*...

    网络游戏-一种六合一数独游戏盒.zip

    2. **对角线数独**:除了标准的行和列要求外,还需要确保主对角线上的数字也不重复。 3. **不规则数独**:方格的形状不再局限于3x3的宫格,而是可以是各种不规则形状,增加了游戏的复杂性和趣味性。 4. **对称数独...

    网络游戏-一种多功能八合一数独游戏棋.zip

    8. **X数独**:结合了经典数独和对角数独的特点,增加了X型的对角线限制。 通过这些不同模式的数独游戏,玩家可以体验到更加丰富多样的解谜乐趣,同时也能不断提升逻辑推理和空间感知能力。无论是对于儿童的智力...

    带附加条件的数独解题程序

    这些灰色数字不仅要遵循标准数独的规则,还要满足额外的条件:它们必须大于其所在的3×3宫格内所有相邻的白色数字(相邻指的是上下左右四个方向,不包括对角线上的数字)。这样的设计无疑增加了解题的难度,因为玩家...

    杀手数独1答案.doc

    它在标准数独的基础上增加了额外的条件,使得解题过程更具挑战性。杀手数独的基本规则是: 1. **基本规则**:与普通数独相同,玩家需要将数字1至9填入9x9的网格中,确保每行、每列以及9个3x3的小宫格(也称为区块)...

    杀手数独题目.doc

    这种类型的数独通常在每个小九宫格(宫)内,用粗线画出一个“X”形状或者两个对角线,这些线条将宫分成了四个部分,称为“区域”。每个区域上会有一个数字,表示该区域内数字出现的次数。 在标准数独中,目标是填...

    C#做的数独求解

    5. **优化策略**:高级的数独求解器可能会使用启发式策略,如对角线检查或预处理已知区域,以减少回溯次数,提高求解效率。 在C#中,这些功能可以通过类和方法来实现。例如,可以创建一个`SudokuSolver`类,其中...

    数独题目高级50题(后附答案).doc

    6. **对角线数独**:某些数独变体包含对角线限制,要求主对角线和副对角线上的数字也要满足唯一性,增加了解题难度。 每个题目后面都有一个完成时间的空栏,这是用来记录玩家解题所用的时间。挑战这些高难度数独...

Global site tag (gtag.js) - Google Analytics