编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。
空白格用 '.'
表示。
一个数独。
答案被标成红色。
Note:
- 给定的数独序列只包含数字
1-9
和字符'.'
。 - 你可以假设给定的数独只有唯一解。
- 给定数独永远是
9x9
形式的。
解题思路
这个之前的问题Leetcode 51:N皇后(最详细的解法!!!)其实是一样的,都是通过回溯法来解。我们就要判断我们每次摆放数字是不是有效,我们只要进行如下三种判断即可
- 所有行是不是满足条件
- 所有列是不是满足条件
- 当前的小正方型是不是满足条件
我们定义函数isValid(row, col, c)
用于判断row
行上是不是有字符c
,判断col
列上是不是有字符c
,row
和col
所在的小正方形内是不是有c
。
这里唯一需要注意的就是小正方形内的坐标变换非常需要技巧,参考Leetcode 36:有效的数独(超详细的解法!!!)中的处理思路,每个小方格看成(row//3,col//3)
,然后小方格内的每个元素坐标就是(row//3*3+i//3,col//3*3+i%3)
。
def isValid(self, board, row, col, c):
for i in range(9):
if board[i][col] == c or board[row][i] == c or\
board[row//3*3+i//3][col//3*3+i%3] == c:
return False
return True
接着我们只要从到位遍历board
,如果当前的位置不是"."
的话,我们就一次放入"123456789"
挨个尝试。
class Solution:
def solveSudoku(self, board):
"""
:type board: List[List[str]]
:rtype: void Do not return anything, modify board in-place instead.
"""
for i in range(9):
for j in range(9):
if board[i][j] != '.':
continue
for c in "123456789":
if self.isValid(board, i, j, c):
board[i][j] = c
if self.solveSudoku(board):
return True
board[i][j] = '.'
return False
return True
def isValid(self, board, row, col, c):
for i in range(9):
if board[i][col] == c or board[row][i] == c or\
board[row//3*3+i//3][col//3*3+i%3] == c:
return False
return True
上面这个算法对于一般的应试来说是足够了的,但是这种思路不够快。那么对于这种dfs
的问题,一个比较好的策略就是开始的搜索范围最好尽量小(对于这个问题中就是尽量优先填充有较少选择的点),实际上我们在解数独问题的时候也是使用的这种策略。
那么如何确定哪些点选择较少呢?我们可以通过row[x]
记录x
行未出现的数,通过col[y]
记录y
列未出现的数,通过cell[x//3][y//3]
记录(x,y)
对应小正方形内未出现的数,那么当前数的可选范围就是(row[x]&col[y]&cell[x//3][y//3])
。
那么现在要怎么处理row
、col
和cell
的存储呢?因为要使用交集
,所以很显然使用set
这个结构(当然也可以使用位运算)。
from collections import defaultdict
class Solution:
def solveSudoku(self, b: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
U = set([str(i) for i in range(1, 10)])
row, col, cell = defaultdict(set), defaultdict(set), defaultdict(set)
def get(i, j):
return (U-row[i]) & (U-col[j]) & (U-cell[i//3,j//3])
def dfs(cur):
if not cur:
return True
minv, x, y = 10, 0, 0
for i in range(9): # 每次寻找最少的搜索范围
for j in range(9):
if b[i][j] == '.':
tc = len(get(i, j))
if len(get(i, j)) < minv:
minv, x, y = tc, i, j
for i in get(x, y):
row[x].add(i)
col[y].add(i)
cell[x//3,y//3].add(i)
b[x][y] = i
if dfs(cur - 1):
return True
row[x].remove(i)
col[y].remove(i)
cell[x//3,y//3].remove(i)
b[x][y] = '.'
return False
for i in range(9):#计算每行、每列、每个单元格的数字个数
for j in range(9):
if b[i][j] != '.':
row[i].add(b[i][j])
col[j].add(b[i][j])
cell[i//3,j//3].add(b[i][j])
dfs(res)
上面的代码思路很清晰,比之前的版本快了十几倍,但是依旧不是最好的写法。
我们可以将上面代码中每次寻找最少的搜索范围进行优化,可以通过一个结构存储每个'.'
的可填充数。每次递归的时候查找最少的可搜索元素minv
,然后将该位置删除(因为这个位置将要填充元素)。接着遍历最少的可搜索元素minv
中的每个数i
,删除存储结构中横纵坐标或者小方格坐标和删除位置相同的元素(因为相同的横纵坐标和小方格内不能再使用该数)。
from collections import defaultdict
class Solution:
def solveSudoku(self, b: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
U = set([str(i) for i in range(1, 10)])
row, col, cell, cnt = defaultdict(set), defaultdict(set), defaultdict(set), defaultdict(set)
def get(i, j):
return (U-row[i]) & (U-col[j]) & (U-cell[i//3,j//3])
def dfs(cur):
if not cur:
return True
x, y = min(cnt, key=lambda n: len(cnt[n]))# 计算最少的搜索范围
minv = cnt[x,y]
del cnt[x,y]
for i in minv:
to_update = []
for k, v in cnt.items(): #删除其他位置与之重叠的元素
if i in v and (k[0] == x or k[1] == y or \
(x//3, y//3) == (k[0]//3, k[1]//3)):
v.remove(i)
to_update.append(k)
b[x][y] = i
if dfs(cur - 1):
return True
b[x][y] = '.'
for pos in to_update:
cnt[pos].add(i)
cnt[x,y] = minv
return False
for i in range(9):
for j in range(9):
if b[i][j] != '.':
row[i].add(b[i][j])
col[j].add(b[i][j])
cell[i//3,j//3].add(b[i][j])
res = 0
for i in range(9):
for j in range(9):
if b[i][j] == '.':
cnt[i,j] = get(i, j)
res += 1
dfs(res)
这个代码的效率比前一个版本又快了一倍,非常优秀!。
我将该问题的其他语言版本添加到了我的GitHub Leetcode
如有问题,希望大家指出!!!