Sudoku Solver
Bảng sudoku là một lưới 9 × 9 được chia thành chín ô vuông 3 × 3. Trong bảng đã giải, mỗi hàng, mỗi cột và mỗi ô vuông đều chứa mỗi chữ số từ 1 đến 9 đúng một lần.
Đề bài gồm 9 chuỗi, mỗi chuỗi tương ứng với một hàng, bắt đầu từ trên cùng; . biểu thị một ô trống. Điền vào tất cả ô trống và trả về bảng đã giải theo cùng định dạng. Bảng có đúng một lời giải.
Hàm
- boardstring-array
- 9 hàng của câu đố, mỗi ô trống có một dấu chấm
- Trả vềstring-array
- 9 hàng của bảng đã giải
Ràng buộc
boardchứa 9 chuỗi có độ dài 9.- Mỗi ký tự là
.hoặc một trong các chữ số từ1đến9. - Các chữ số đã cho không bao giờ lặp lại trong cùng một hàng, cột hoặc ô.
- Bảng có đúng một lời giải.
Ví dụ
- Đầu vào
- board = ["9.7.....6", "824.3....", "..1..9..2", ".9...2.18", "...4....5", "1325.8...", "...687...", "......8.9", ".4.9...5."]
- Đầu ra
- ["957214386", "824736591", "361859742", "495372618", "786491235", "132568974", "519687423", "273145869", "648923157"]
- Giải thích
- Hàng trên cùng
9.7.....6đã có 9, 7 và 6, nên sáu ô trống còn lại sẽ điền các số 1, 2, 3, 4, 5 và 8. Các cột và ô vuông sẽ quyết định vị trí của từng số, và hàng sẽ trở thành957214386.
Câu hỏi mở rộng
Trong killer sudoku, tổng các ô trong mỗi lồng cũng phải bằng tổng đã cho. Bạn sẽ thêm quy tắc đó vào quá trình tìm kiếm như thế nào và dùng nó để cắt bỏ các nhánh từ sớm?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Theo dõi các chữ số đã có trong từng hàng, từng cột và từng ô 3 × 3. Sau đó, bạn có thể xác định trong O(1) liệu một chữ số có thể điền vào ô trống hay không.
Điền từng ô trống một. Thử từng chữ số phù hợp với ô, chuyển sang ô tiếp theo và xóa chữ số đó nếu không thể hoàn thành phần còn lại của bảng.
Thứ tự các ô rất quan trọng. Tiếp theo, hãy điền vào ô trống còn ít chữ số có thể chọn nhất: ô chỉ có một lựa chọn không cần phân nhánh, còn ô không có lựa chọn nào cho bạn biết ngay rằng cần quay lui.
Lời giải
Giải bằng cách quay lui: điền một chữ số phù hợp, tiếp tục, rồi xóa chữ số đó nếu không thể hoàn thành bảng. Thứ tự các ô quyết định tốc độ. Với một bảng khó, duyệt theo thứ tự đọc có thể đi qua hàng trăm triệu bảng chưa hoàn chỉnh; điền ô có ít lựa chọn nhất trước chỉ cần khoảng một trăm bảng.
Quay lui theo thứ tự đọc
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Điền vào các ô trống theo thứ tự đọc, mỗi ô chọn chữ số nhỏ nhất phù hợp. Khi một ô không còn chữ số nào để thử, quay lại ô trước đó và thử chữ số tiếp theo. Lần lượt thử mọi tổ hợp tuân theo quy tắc, nhờ đó quá trình tìm kiếm sẽ tìm ra lời giải.
Ba bảng giúp mỗi lần kiểm tra độ phù hợp chỉ mất O(1): rowUsed[r][d], colUsed[c][d] và boxUsed[b][d], với ô vuông b = (r / 3) * 3 + c / 3. Khi đặt một chữ số, ta đặt ba mục thành đúng; khi xóa chữ số đó, ta xóa các mục ấy.
Thứ tự cố định là điểm yếu: quá trình tìm kiếm có thể điền đầy một hàng bằng các chữ số phù hợp tại chỗ, rồi mãi sau mới phát hiện rằng chúng khiến nhiều ô không còn chỗ để điền. Một bảng có các hàng trên cùng gần như không có gợi ý, với đáp án là các chữ số lớn, khiến quá trình tìm kiếm phải duyệt hơn 300 triệu bảng chưa hoàn chỉnh.
Thuật toán
- Đọc bảng thành một lưới với 0 cho ô trống, điền
rowUsed,colUsedvàboxUsed, rồi liệt kê các ô trống theo thứ tự đọc. backtrack(k): nếukbằng số ô trống, trả về true.- Với mỗi chữ số
dtừ 1 đến 9 chưa được dùng trong hàng, cột và ô vuông chứa ôk, hãy đặt chữ số đó vào ô, đánh dấu nó và trả về true nếubacktrack(k+1)trả về true; nếu không, bỏ đánh dấu. - Nếu không có chữ số nào phù hợp, xóa nội dung ô và trả về false. Gọi
backtrack(0)rồi chuyển lưới trở lại thành các chuỗi.
def solveSudoku(board):
grid = [[0 if ch == "." else int(ch) for ch in row] for row in board]
# row_used[r][d] is True when digit d already sits in row r; same for
# columns and boxes. Box b holds the cells with (r // 3) * 3 + c // 3 == b.
row_used = [[False] * 10 for _ in range(9)]
col_used = [[False] * 10 for _ in range(9)]
box_used = [[False] * 10 for _ in range(9)]
empties = []
for r in range(9):
for c in range(9):
d = grid[r][c]
if d:
row_used[r][d] = col_used[c][d] = box_used[(r // 3) * 3 + c // 3][d] = True
else:
empties.append((r, c))
def backtrack(k):
if k == len(empties):
return True
r, c = empties[k]
b = (r // 3) * 3 + c // 3
for d in range(1, 10):
if not row_used[r][d] and not col_used[c][d] and not box_used[b][d]:
grid[r][c] = d
row_used[r][d] = col_used[c][d] = box_used[b][d] = True
if backtrack(k + 1):
return True
row_used[r][d] = col_used[c][d] = box_used[b][d] = False
grid[r][c] = 0
return False
backtrack(0)
return ["".join(str(d) for d in row) for row in grid]Điền vào ô trống nhất trước
Ý tưởng
Lời giải không phụ thuộc vào thứ tự điền, vì vậy hãy chọn thứ tự giúp phạm vi tìm kiếm nhỏ nhất: ở mỗi bước, điền vào ô trống có ít chữ số hợp lệ nhất. Ô chỉ có một chữ số hợp lệ là ô bắt buộc, không tạo thêm nhánh nào. Ô không có chữ số hợp lệ nào sẽ được chọn ngay, nhờ đó phát hiện bàn cờ sai lập tức.
Để đếm nhanh các chữ số hợp lệ cho 81 ô ở mỗi bước, hãy lưu mỗi hàng, cột và khối dưới dạng bitmask: bit d của rows[r] được bật khi chữ số d nằm trong hàng r. Một ô cho phép ~(rows[r] | cols[c] | boxes[b]) & 0x3FE, trong đó 0x3FE giữ lại các bit từ 1 đến 9, còn số lượng là số bit được bật. Đặt d sẽ bật bit d trong ba mask; phép XOR loại bỏ bit đó.
Thuật toán
- Tạo lưới và các mặt nạ
rows,colsvàboxestừ các chữ số đã cho. backtrack(): quét tất cả các ô, tính các chữ số hợp lệ cho từng ô trống và chọn ô đầu tiên có số lượng nhỏ nhất. Nếu không còn ô trống, trả về true.- Với mỗi chữ số hợp lệ của ô đó, điền chữ số vào ô, bật bit tương ứng trong cả ba mặt nạ và trả về true nếu
backtrack()trả về true; nếu không thì xóa các bit đó. - Nếu không có chữ số nào phù hợp, để trống ô đó và trả về false. Gọi
backtrack()rồi chuyển lưới trở lại thành các chuỗi.
def solveSudoku(board):
grid = [[0 if ch == "." else int(ch) for ch in row] for row in board]
# Bit d of rows[r] is set when digit d already sits in row r; same for
# columns and boxes. Box b holds the cells with (r // 3) * 3 + c // 3 == b.
rows, cols, boxes = [0] * 9, [0] * 9, [0] * 9
for r in range(9):
for c in range(9):
d = grid[r][c]
if d:
rows[r] |= 1 << d
cols[c] |= 1 << d
boxes[(r // 3) * 3 + c // 3] |= 1 << d
def backtrack():
# Pick the empty cell with the fewest digits left (the first one on a tie).
best_r, best_c, best_allowed, best_count = -1, -1, 0, 10
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
# Bits 1 to 9 of 0x3FE are the nine digits.
allowed = ~(rows[r] | cols[c] | boxes[(r // 3) * 3 + c // 3]) & 0x3FE
count = bin(allowed).count("1")
if count < best_count:
best_r, best_c, best_allowed, best_count = r, c, allowed, count
if best_r == -1:
return True # no empty cell is left: the board is solved
r, c = best_r, best_c
b = (r // 3) * 3 + c // 3
for d in range(1, 10):
bit = 1 << d
if best_allowed & bit:
grid[r][c] = d
rows[r] |= bit
cols[c] |= bit
boxes[b] |= bit
if backtrack():
return True
rows[r] ^= bit
cols[c] ^= bit
boxes[b] ^= bit
grid[r][c] = 0
return False
backtrack()
return ["".join(str(d) for d in row) for row in grid]
Lỗi thường gặp và trường hợp biên
- Quên xóa chữ số khỏi các bản ghi hàng, cột và ô vuông khi bạn hoàn tác. Các ô tiếp theo sẽ xem chữ số đó là đã dùng, khiến quá trình tìm kiếm bỏ lỡ lời giải.
- Dùng
r / 3 + c / 3để xác định ô vuông. Cả ô(0, 4)và(4, 0)đều có kết quả là 1, nên hai ô vuông dùng chung một bản ghi và bảng không thể được điền kín. Hãy dùng(r / 3) * 3 + c / 3với phép chia số nguyên. - Không dừng lại ở lời giải đầu tiên. Các bước hoàn tác sẽ làm trống bảng lần nữa trước khi bạn đọc kết quả.
Câu hỏi thường gặp3
Độ phức tạp thời gian của thuật toán giải sudoku bằng quay lui là bao nhiêu?
Độ phức tạp theo cấp số nhân trong trường hợp xấu nhất, O(9^m) với m ô trống, vì mỗi ô có thể phân nhánh thành tối đa 9 chữ số. Với các câu đố thực tế, quy tắc về hàng, cột và khối loại bỏ hầu hết các nhánh, còn việc điền ô trống nhất trước sẽ loại bỏ phần lớn số nhánh còn lại.
Làm thế nào để kiểm tra xem một câu đố sudoku có đúng một lời giải?
Chạy cùng một quá trình tìm kiếm quay lui, nhưng đếm số lời giải và dừng khi số đếm đạt đến 2. Số đếm bằng 1 nghĩa là câu đố có lời giải duy nhất, bằng 0 nghĩa là câu đố không có lời giải. Trình tạo câu đố chạy bước kiểm tra này sau khi xóa từng gợi ý.
Có cách nào giải sudoku nhanh hơn thuật toán quay lui không?
Hầu hết người giải nhanh vẫn quay lui, nhưng suy luận được nhiều hơn trước mỗi lần đoán. Lan truyền ràng buộc điền vào mọi ô chỉ có một lựa chọn và mọi chữ số chỉ khớp với một ô trong hàng, cột hoặc khối của ô đó, rồi lặp lại. Sudoku cũng có thể được biểu diễn thành bài toán phủ chính xác và giải bằng Thuật toán X của Knuth.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def solveSudoku(board):
# Viết code vào đâyKiểm thử #1
Đối số
board = ["9.7.....6", "824.3....", "..1..9..2", ".9...2.18", "...4....5", "1325.8...", "...687...", "......8.9", ".4.9...5."]
Đầu ra mong đợi
["957214386", "824736591", "361859742", "495372618", "786491235", "132568974", "519687423", "273145869", "648923157"]