数独,相信很多朋友小时候都玩过。数独一般是9x9的一个数字矩阵,其中具有三个规则
- 每一行都包含1-9的所有数字
- 每一列都包含1-9的所有数字
- 每个下小矩阵都包含1-9的所有数字
由于每行,列,小矩阵一共只包含9个空,所以隐含要求就是不能重复。而这个不能重复正式我们做数独的关键思路所在。
贪心做法
拿一个简单版的数独举个例子
我一般手工的做法是什么呢?以空[0,2]为例,按照上面三个规则,首先该空所有的可能情况是1-9的一个集合,现在减去该行(第0行)存在的数字,还剩下1,3,5,6,7这五个数字,再减去该列(第2列)存在的数字,还剩下6这一个数字,再减去它所在的这个矩阵存在的数字,也就还剩下6,那么我们此时就可以将6填到[0,2]这个坐标的空格中。以此类推。(此做法有坑,难度提高没有解)
我们先按照刚才的思路,将代码写出来,然后再来分析里面的坑。
首先,我们需要定义一个函数来获取某个空所有可能出现的值集合
# 得到第 i 行的所有数字
def get_row_number(i: int, old1):
ans = set()
for x in old1[i]:
if x != 0:
ans.add(x.__str__())
return ans
# 得到第j列的所有数字
def get_column_number(j: int, old1):
ans = set()
for i in range(0, len(old1)):
if old1[i][j] != 0:
ans.add(old1[i][j].__str__())
return ans
# 得到[i,j]所在矩阵的所有数字
def get_zhengfangxing_number(i: int, j: int, old1):
x = int(i / 3) * 3
y = int(j / 3) * 3
ans = set()
for x_begin in range(x, x + 3):
for y_begin in range(y, y + 3):
if old1[x_begin][y_begin] != 0:
ans.add(old1[x_begin][y_begin].__str__())
return ans
def get_one_ans(i: int, j: int, old1):
zhengfangxing = get_zhengfangxing_number(i, j, old1)
column = get_column_number(j, old1)
row = get_row_number(i, old1)
ans = able_number - zhengfangxing
ans = ans - column
ans = ans - row
return ans
上面这个函数中,比较麻烦的点是确定某个空在哪个小矩阵里面,结论是对坐标除以3然后将结果进行向下取整之后的结果再乘以3,有兴趣的朋友可以推导一下。
然后,我们需要遍历每个空,找出所有空的可能的取值集合,一旦有集合大小为1,我们就可以直接将结果填进去。如果填了一次之后,没有将空填完,我们就开始第二次遍历(第二次遍历基于第一次遍历填好的结果),直到所有空都填完。代码可以这样写:
def print_old(old):
n = len(old)
for i in range(0, n):
m = len(old[i])
ans = ''
for j in range(0, m):
ans = ans + old[i][j].__str__() + ','
print(ans)
# 此函数判断矩阵中是否还包含没有填的
# 这里使用数字0代码没有填
def check_0(old):
for x in old:
if 0 in x:
return True
def get_result(old):
cnt = 0
while check_0(old):
n = len(old)
for i in range(0, n):
m = len(old[i])
for j in range(0, m):
if old[i][j] == 0:
ans = get_one_ans(i, j,old)
if len(ans) == 1:
for x in ans:
old[i][j] = x
print_old()
cnt += 1
print(cnt)
正如上面说到的一样,每一次填完之后,都需要判断当前是否还有空没有填,如果有,则继续进入遍历判断。如果没有空可以填,代表算法完成,打印出结果直接退出。
现在我们来测试一下,以上面这道题为例:
遍历了两遍最终的结果就已经得出来了。现在我们填进去看下是否正确。
为啥花了29分钟,大概是因为我在写博客的原因吧。
好了,算法写到这里,真的结束了吗?让我们来试试专家级难度吧!
话不多说,直接用算法跑一跑结果
结果直到遍历到第2973次都没有出(当然遍历时间很短),而且可以发现2973次迭代和2974次迭代的结果一模一样,为啥会出现这种情形?
上面的算法中,我们在每一次迭代的过程中,会将只有一种取值情况的空填上最终的结果,这是一种贪心的思想,因为完全有可能出现,在一次矩阵遍历后,所有空着的点的可能取值集合的大小都大于1,那么这轮遍历我们就无法继续深入下去,下一轮下下轮都只能是同样的结果。
一般做法
上面说到了,我们遍历了所有空,没有一个空可能的取值集合大小等于1,那么我们能做的就是枚举所有空的所有取值,直到得到一种取值场面,使得所有的空都填上。
这种搜索算法,就跟走迷宫类似,你需要遍历每一条路,直到找到出口为止。也就是俗称的深度优先遍历。在写代码之前,我们需要确认两个东西
第一,函数搜索成功,返回Ture的条件是什么?
第二,函数搜索过程中,怎么判断某次尝试是否一定错误的?
第一个问题,和贪心做法一样,我们每次尝试都需要判断当前场面下是否还有空没有填,如果没有了,就说明整个搜索算法已经找打了解,就可以直接返回了
第二个问题,我们在做了每次尝试之后,需要计算当前场面所有空的所有可能取值,如果得出存在一个空,所有可能的取值集合的大小为0,那么就说明该次尝试是一定错误的,就直接返回false,退回的上层再尝试。
成功和失败的退出条件都找到了,我们可以直接写代码了
def get_result(old1):
# 成功退出的条件判断
if not check_0(old1):
print_old(old1)
return True
n = len(old1)
flag = 0
# 失败退出的条件判断
for i in range(0, n):
m = len(old1[i])
for j in range(0, m):
if old1[i][j] == 0:
ans = get_one_ans(i, j, old1)
if len(ans) == 0:
flag = 1
break
if flag == 1:
break
if flag == 1:
# todo 说明出现了错误,需要回退
return False
# 尝试的算法流程控制
for i in range(0, n):
m = len(old1[i])
for j in range(0, m):
if old1[i][j] == 0:
ans = get_one_ans(i, j,old1)
for x in ans:
new = old1
new[i][j] = x
# 这里进行下层节点尝试,这是一个递归的过程,相当于将问题交给子问题,有点动态规划的感觉,即如果子问题找到了解,那么父问题也找到了解
if get_result(new):
return True
new[i][j] = 0
old1[i][j] = 0
return False
return False
递归过程对于没有学过数据结构的朋友理解上可能有点困难,可以先了解一下一棵树的几种遍历顺序,前中后。
下面我们用这个算法来跑一跑上面那个专家级难度的数独题目
话不多说,去网站试一下是否正确
值得注意的是,如果你拿到这份代码测试的时候发现答案是错误的,那么请你重新再把题目输入一次(我在写这份博客的时候,就不慎输入错了三次,0代表空)
来源:oschina
链接:https://my.oschina.net/u/3773302/blog/4817176