T37 解数独
题目描述如下:
做题思路:先想着有没有可以利用的规则,但是想到数独的确定的规则就如题中所给三个,确实没有简单规则来做捷径,因此确定了需要遍历所有解空间,但是在遍历的过程中会出现很多不符合要求的情况,并且从不符合要求的情况开始就不需要继续往下做了,因为当前这个错了,之后的也不可能对,也就是说可以剪枝,减去很多不必要的运算。
因此,每次往board中第一个“.”放1-9试试看,如果能放1,那么在第二个“.”处再放放1试试看,对于所有的“.”地方,都会放1-9试一次(没必要的解就剪枝不会去试),直到出现符合要求的解。伪代码思路如下:
def func(board):
# 判断在横竖位置,放入i,j位置的数值是否满足要求,也就是规则1,2
def RCLine(board,i,j)
# 判断在粗线分割的位置,放入i,j位置的数值是否满足要求,也就是规则3
def NineCells(board,i,j)
# 判断是否递归结束,结束条件为board内摆满了数字
def isEnd(board)
# 判断是否递归结束
isEnd(board)
# 开始递归1-9数字
for k in range(1,10):
board[i][j]=k
# 判断该位置是否可以放
RCLine(board,i,j)
NineCells(board,i,j)
# 如果可以放
if flags is True:
# 继续递归
func(board)
# 如果解空间该分支满足递归结束要求,则返回上一递归
if isEnd(board):
return
# 如果该分支的解都不满足,则需要还原该分支1—9的填涂,防止影响之后的计算
board[i][j] = "."
整体代码如下:
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
# 判断是否行列上满足只有一次1-9
def RCLine(board, i, j):
num = board[i][j]
rnums = 0
cnums = 0
# 计算行列内该数出现的次数
for k in range(0, 9):
if board[i][k] == board[i][j]:
rnums += 1
if board[k][j] == board[i][j]:
cnums += 1
if rnums > 1 or cnums > 1:
return False
else:
return True
# 判断是否九宫格内满足只有一次1-9
def NineCells(board, i, j):
num = board[i][j]
# 将srow放到九宫格内的中心位置,方便检索
t = int(i / 3)
srow = 1 + t*3
# 将scol放到九宫格内的中心位置,方便检索
t = int(j / 3)
scol = 1 + t*3
# 检查九宫格范围内是否出现重复
flag = 0
for i in range(-1, 2):
for j in range(-1, 2):
if board[srow + i][scol + j] == num:
flag += 1
if flag > 1:
return False
else:
return True
# 判断是否结束递归
def isEnd(board):
for i in range(9):
for j in range(9):
if board[i][j] == ".":
return i, j, False
return 0,0,True
# 找到第i个为“.”的位置
thei, thej, flag = isEnd(board)
# 若未找到说明结束找到则递归
if flag == True:
return
# 开始递归
flag2 = 0
for i in range(1, 10):
board[thei][thej] = str(i)
RCLineFT = RCLine(board, thei, thej)
NineCellsFT = NineCells(board, thei, thej)
if RCLineFT is True and NineCellsFT is True:
self.solveSudoku(board)
tmpi, tmpj, TF = isEnd(board)
if TF is True:
return
else:
continue
board[thei][thej] = "."
提交结果如下:
发现这个做法执行用时太长了,想想该怎么优化:
1.递归是否可以化成迭代,减少函数的调用过程
2.每次检查是否满足时,每次递归都尝试了一次1-9,都检索了一次一行一列一九宫格,然而其实只需要判断该元素是否在该行该列该九宫格的集合内就可以了,其实有些地方是不需要尝试1-9的,因为该行列上已经有一些数字了,只需要尝试其中没有的数字,这样可以减少规则判断函数的使用次数,相当于把判断提前了。用集合实现,只需要判断尝试不在行列以及所在九宫格内的集合内的元素就可以,实现了状态压缩
其中第一个方法我查看了下大佬的代码,好像都是用的递归,我也就没有尝试了,改进后代码如下:
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
def getIndex(i, j):
Ni = int(i / 3)
Nj = int(j / 3)
# index是在集合中的行列九宫格索引
index1 = i
index2 = 9 + j
index3 = 18 + Ni * 3 + Nj
return index1, index2, index3
# 添加表上元素操作时需要对集合进行相应的操作
def setAddNum(sets, i, j, num):
index1, index2, index3 = getIndex(i,j)
sets[index1].add(num)
sets[index2].add(num)
sets[index3].add(num)
# 删除表上元素操作时需要对集合进行相应的操作
def setRemoveNum(sets, i, j, num):
index1, index2, index3 = getIndex(i,j)
sets[index1].remove(num)
sets[index2].remove(num)
sets[index3].remove(num)
#判断是否结束,未结束返回第一个为“.”的下标
def isEnd(board):
for i in range(9):
for j in range(9):
if board[i][j] == ".":
return i, j, False
return 0, 0, True
#递归函数
def dfs(board, sets):
thei, thej, flag = isEnd(board)
# 若未找到说明结束找到则递归
if flag == True:
return
index1, index2, index3 = getIndex(thei, thej)
searchSet = allnum - (mySets[index1] | mySets[index2] | mySets[index3])
for m in searchSet:
board[thei][thej] = m
setAddNum(sets,thei,thej,m)
dfs(board, sets)
i, j, flag = isEnd(board)
if flag is True:
return
else:
setRemoveNum(sets,thei,thej,m)
board[thei][thej] = "."
#这里是solveSudoku函数,用于初始化一些集合,再调用递归函数
allnum = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}
# mySets数据结构
# 前9个set为1-9行各行的数字集合
# 中间9个set为1-9列各列的数字集合
# 后9个set为1-9个九宫格的数字集合 123\n456\n789
mySets = [set() for i in range(27)]
for i in range(9):
for j in range(9):
if board[i][j] != ".":
index1, index2, index3 = getIndex(i, j)
mySets[index1].add(board[i][j])
mySets[index2].add(board[i][j])
mySets[index3].add(board[i][j])
# 开始递归
dfs(board, mySets)
结果如下:(达到平均水平了,嘿嘿)
参考大佬代码后发现还能继续优化:每次都使用 getNext()
选择能填的数字最少的格子开始填,这样填错的概率最小,回溯次数也会变少,也有其他解数独的解法的应用,总的说来是用解数独的知识来减少不必要的运算。