96题链接
数独,一个很经典的填数字游戏。规则也很简单,每一行、每一列、九个九宫格的九个数字不重复。 设计很好的数独游戏只有一个解,题目中也假设了这一点。 题目最后要求五十个数独的解,然后计算左上角三位数之和。 这个题对我来说还算比较简单,只需要把我人肉做数独游戏的方法写成程序就好了。
由于整体思路非常清晰,我采用了自上而下的设计方式,几乎一气呵成了整个程序,除了debug了一个小地方fix了一个bug外。 我需要一个函数把题目给的txt下载下来并一行一行的读到一系列二位数组中。 对于每个二维数组,就是一个数独题目,然后求解,得到左上角的三位数。求和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static readonly int Length = 9 ;public static int GetAnswer ( ){ int sum = 0 ; var lines = GetContent(); for (int i = 0 ; i < 50 ; i++) { int [,] soDoku = new int [Length, Length]; for (int j = 0 ; j < Length; j++) { var line = lines[10 * i + j + 1 ]; for (int k = 0 ; k < Length; k++) { soDoku[j, k] = Convert.ToInt32(line[k].ToString()); } } sum += GetTopLeft(SolveSuDoku(soDoku)); } return sum; }
GetContent()
函数很简单,下载文件,切分行。如果愿意的话,甚至能写成一行。
1 2 3 4 5 6 private static string [] GetContent ( ){ WebClient client = new WebClient(); string content = client.DownloadStri("https://projecteuler.net/project/resources/p096_sudoktxt" ); return content.Split('\n' ); }
GetTopLeft(int[,] soDoku)
函数相对也比较简单,拿到二维数组的左上角三个数字,拼成string
然后转成int
即可。或者是第一位乘以100加第二位乘以10加第三位。
1 2 3 4 private static int GetTopLeft (int [,] soDoku ){ return int .Parse($"{soDoku[0 , 0 ]} {soDoku[0 , 1 ]} {soDoku[0 , 2 ]} " ); }
最复杂的部分就是解数独了。 解数独就是在不停的猜测和测试,整个过程显然是递归的。 找个一个空,可能有几个情况,选一个填进去,依旧是未完成的数组,再来一轮,一轮,直到完成。整个过程是一个树,分了很多叉。题目告诉我们只有一个解,那么只有一个叶子节点是完整的解,其他叶子节点都不合法,有矛盾的地方,比如某个点所在的行和列出现了重复数字。 首先,我们判断数独是否完成,如果是,则返回该解。 如果没有,我们需要找一个点,该点没有被填写数字,且可能填的数字尽可能的少,这种情况下该节点的子树才最少,效率能尽可能的高。FindMinimalPoint
返回参数包含三项,前两者描述的该节点的位置,第三项是可能的值。如果Item3
是空的话,那么说明当前的数独有矛盾,返回null
。试图尝试所有的可能性,如果有解,则返回,如果所有的可能性都无解的话,走到最后一行返回null
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static int [,] SolveSuDoku(int [,] soDoku){ if (IsCompleted(soDoku)) { return soDoku; } var point = FindMinimalPoint(soDoku); foreach (var v in point.Item3) { int [,] newSoDoku = soDoku.Clone() as int [,]; newSoDoku[point.Item1, point.Item2] = v; var ret = SolveSuDoku(newSoDoku); if (ret != null ) { return ret; } } return null ; }
如果实现FindMinimalPoint
呢?遍历非0点,找到可能的值(FindVaulesAt
),记录下可能的值最少的点坐标和对应的值并返回。对于FindVaulesAt
函数,很可能返回0个解,就是说该数独有矛盾。minList
在初始化的时候写了十个值,目的是比九大,其实这里写九个也没问题,因为最小值肯定比九要小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static (int , int , List<int > ) FindMinimalPoint (int [,] soDoku ){ int minRow = -1 ; int minCol = -1 ; var minList = new List<int >() { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; for (int i = 0 ; i < Length; i++) { for (int j = 0 ; j < Length; j++) { if (soDoku[i, j] == 0 ) { var values = FindVaulesAt(i, j, soDoku); if (values.Count < minList.Count) { minList = values; minRow = i; minCol = j; } } } } return (minRow, minCol, minList); }
FindVaulesAt
想法很直接。每个空可能填入一到九九个数字,排除所在行、所在列和所在的九宫格已经出现的数字即可。 文章开头提到fix了一个小bug就是在这个函数,一开始row的初始值写成了i % 3
,row < i - i % 3 + 3
,进而没能得到正确的可能的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static List<int > FindVaulesAt (int i, int j, int [,] soDoku ){ List<int > values = new List<int > { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; for (int k = 0 ; k < Length; k++) { values.Remove(soDoku[i, k]); } for (int k = 0 ; k < Length; k++) { values.Remove(soDoku[k, j]); } for (int row = i - i % 3 ; row < i - i % 3 + 3 ; row++) { for (int col = j - j % 3 ; col < j - j % 3 + 3 ; col++) { values.Remove(soDoku[row, col]); } } return values; }
最后,给出整个拼图的最后一块IsCompleted
。如果所有空都不是0,说明数独已经求解完毕了。
1 2 3 4 5 6 7 8 9 10 11 12 private static bool IsCompleted (int [,] soDoku ){ foreach (var cell in soDoku) { if (cell == 0 ) { return false ; } } return true ; }