Câu đố Sudoku với các hộp chứa số vuông


8

Hai ngày trước, tôi đã gặp một vấn đề về sudoku mà tôi đã cố gắng giải quyết với Python 3. Tôi đã được thông báo rằng một giải pháp tồn tại, nhưng tôi không chắc chắn liệu có tồn tại nhiều giải pháp hay không.

Vấn đề như sau: Một lưới sudoku 9x9 hoàn toàn trống rỗng. Tuy nhiên, nó có chứa các hộp màu và bên trong các hộp này, tổng các số phải là một số vuông . Ngoài ra, các quy tắc sudoku bình thường được áp dụng.

Vấn đề ở đây không phải là giải một câu đố sudoku, mà là tạo ra một câu đố khả thi, thỏa mãn các quy tắc của các hộp màu .

Chiến lược của tôi

Sử dụng mảng numpy, tôi đã chia lưới thành 81 chỉ mục, có thể được sắp xếp lại thành lưới 9x9.

import numpy as np
print(np.array([i for i in range(81)]).reshape((9, 9)))

->
[[ 0  1  2  3  4  5  6  7  8]
 [ 9 10 11 12 13 14 15 16 17]
 [18 19 20 21 22 23 24 25 26]
 [27 28 29 30 31 32 33 34 35]
 [36 37 38 39 40 41 42 43 44]
 [45 46 47 48 49 50 51 52 53]
 [54 55 56 57 58 59 60 61 62]
 [63 64 65 66 67 68 69 70 71]
 [72 73 74 75 76 77 78 79 80]]

Dưới đây là danh sách chứa tất cả các khối chỉ số.

boxes = [[44, 43, 42, 53],[46, 47, 38],[61, 60],[69, 70],[71, 62],
         [0, 9, 18],[1, 10, 11, 20],[2, 3, 12],[4, 13, 14],[5, 6],
         [7, 8],[17, 26, 35],[21, 22, 23],[15, 16, 24, 25, 34],
         [27, 36, 37],[19, 28, 29],[45, 54],[55, 56],[63, 64, 65],
         [72, 73, 74],[57, 66, 75 ],[58, 59, 67, 68],[76, 77],[78, 79, 80]]

Như bạn có thể thấy từ hình ảnh , hoặc từ mảng trên, các hộp được sắp xếp thành các khối 2, 3, 4 hoặc 5 (8 twos, 12 threes, 3 fours, 1 fiver). Tôi cũng nhận thấy rằng một hộp có thể chứa nhiều số mà không vi phạm bất kỳ quy tắc nào của sudoku, nhưng chỉ có 2 trong số một số là có thể. Cho thông tin đó, bình phương lớn nhất có thể sẽ là 36, vì 9 + 9 + 8 + 7 + 6 = 39, và do đó, không có tổng khối nào có thể đạt tới 49. Để tìm hiểu xem tổng của danh sách có chứa số vuông không , Tôi đã thực hiện chức năng sau:

def isSquare(array):
    if np.sum(array) in [i**2 for i in range(1,7)]:
        return True
    else:
        return False

Để tìm hiểu xem danh sách có chứa số lượng trùng lặp chính xác hay không, nghĩa là, nhiều hơn một bản sao chỉ có một số, tôi đã thực hiện chức năng sau:

def twice(array):
    counter = [0]*9
    for i in range(len(array)):
        counter[array[i]-1]+=1
        if 3 in counter:
            return False
    if counter.count(2)>1:
        return False
    return True

Bây giờ, với các chữ số 1-9, có một số cách giới hạn giải pháp cho một danh sách, nếu danh sách phải tổng hợp thành một số vuông. Sử dụng itertools , tôi có thể tìm thấy các giải pháp, chia chúng thành một mảng, trong đó chỉ số 0 chứa các khối twos, chỉ số 1 chứa các khối ba mươi, v.v.

from itertools combinations_with_replacement
solutions = []
for k in range(2, 6):
    solutions.append([list(i) for i in combinations_with_replacement(np.arange(1, 10), k) if 
    isSquare(i) and twice(i)])

Tuy nhiên, bất kỳ hoán vị của các danh sách này là giải pháp khả thi cho "vấn đề bình phương". Sử dụng itertools một lần nữa, tổng số lượng hộp có thể (không có quy tắc sudoku) tổng cộng là 8782.

from itertools import permutations

def find_squares():
    solutions = []
    for k in range(2, 6):
        solutions.append([list(i) for i in combinations_with_replacement(np.arange(1, 10), k) if 
            isSquare(i) and twice(i)])
    s = []
    for item in solutions:
        d=[]
        for arr in item:
            for k in permutations(arr):
                d.append(list(k))
        s.append(d)
    return s # 4-dimensional array, max 2 of each

solutions = find_squares()

total = sum([len(i) for i in solutions])
print(total)
-> 8782

Điều này là đủ để thực hiện chức năng quyết định xem một bảng có hợp pháp hay không, nghĩa là các hàng, cột và hộp chỉ chứa một trong các chữ số 1-9. Thực hiện của tôi:

def legal_row(arr):
    for k in range(len(arr)):
        values = []
        for i in range(len(arr[k])):
            if (arr[k][i] != 0):
                if (arr[k][i] in values):
                    return False
                else:
                    values.append(arr[k][i])
    return True

def legal_column(arr):
    return legal_row(np.array(arr, dtype=int).T)


def legal_box(arr):
    return legal_row(arr.reshape(3,3,3,3).swapaxes(1,2).reshape(9,9))


def legal(arr):
    return (legal_row(arr) and legal_column(arr) and legal_box(arr))

Khó khăn với thời gian chạy

Một cách tiếp cận đơn giản sẽ là kiểm tra mọi sự kết hợp của từng khối. Tôi đã khắc phục điều này và tạo ra một số vấn đề khả thi, tuy nhiên sự phức tạp của thuật toán của tôi khiến việc này mất quá nhiều thời gian.

Thay vào đó, tôi đã cố gắng ngẫu nhiên một số thuộc tính: Thứ tự của các khối và thứ tự của các giải pháp. Sử dụng điều này, tôi đã giới hạn số lần thử và kiểm tra xem một giải pháp có khả thi hay không:

attempts = 1000
correct = 0
possibleBoards = []
for i in range(1, attempts+1):
    board = np.zeros((9, 9), dtype=int)
    score = 0
    shapes = boxes
    np.random.shuffle(shapes)
    for block in shapes:
        new_board = board
        new_1d = board.reshape(81)
        all_sols = solutions[len(block)-2]
        np.random.shuffle(all_sols)
        for sols in all_sols:
            #print(len(sols))
            new_1d[block] = sols
            new_board = new_1d.reshape((9, 9))
            if legal(new_board):
                board = new_board
                score+=1
                break
    confirm = board.reshape(81)
    #solve(board) # Using my solve function, not important here
    # Note that without it, correct would always be 0 as the middle of the puzzle has no boxes
    confirm = board.reshape(81)
    if (i%1000==0 or i==1):
        print("Attempt",i)
    if 0 not in confirm:
        correct+=1
        print(correct)
        possibleBoards.append(board)

Trong đoạn mã trên, điểm số biến có nghĩa là có bao nhiêu khối thuật toán có thể tìm thấy trong một lần thử. Biến chính xác đề cập đến có bao nhiêu bảng sudoku được tạo có thể được hoàn thành. Nếu bạn quan tâm đến việc nó đã làm tốt như thế nào trong 700 lần thử, thì đây là một số thống kê (Đây là một lịch sử, trục x đại diện cho điểm số và trục y biểu thị số lượng mỗi điểm có trong 700 lần thử này).

Tôi cần giúp gì

Tôi đang vật lộn để tìm một cách khả thi để tìm ra giải pháp cho vấn đề này, đó thực sự có thể chạy trong một khoảng thời gian hữu hạn. Tôi sẽ đánh giá rất cao bất kỳ mẹo nào liên quan đến việc làm cho một số mã của tôi nhanh hơn hoặc tốt hơn, mọi ý tưởng về cách tiếp cận khác nhau cho vấn đề, mọi giải pháp cho vấn đề hoặc một số mẹo hữu ích về Python / Numpy liên quan đến vấn đề này.


1
isSquare(a): return math.sqrt(sum(a)) % 1.0 == 0hoặc là. Khá chắc chắn rằng đó là cách nhanh hơn so với tính toán lại [i**2 for i in range(1,7)]mỗi lần và thực hiện tìm kiếm tuyến tính trong đó. Ngoài ra, inđã trả về một boolean. Không cần cho if.
Nhà vật lý điên

Hãy nhớ rằng phần giải pháp = find_squares () chỉ mất chưa đến một giây, đây là phần cuối cùng, việc thực hiện mà tôi phải tìm hiểu xem câu trả lời có đúng hay không.
đường

1
FYI (cũng cho bất kỳ ai khác đọc điều này), bạn có thể xem giải thích về câu đố tại đây: youtube.com/watch?v=myGqOF6blPI và có một liên kết trong mô tả để chơi trực tuyến. Đó là một câu đố tuyệt vời và kênh đó là tuyệt vời. Tôi thực sự đã giải câu đố này ngày hôm qua vì vậy tôi rất ngạc nhiên khi thấy điều này!
Alex Hall

Câu trả lời:


5

Đây là nơi tôi sẽ sử dụng một bộ giải SMT. Họ mạnh hơn rất nhiều so với những người cho tín dụng. Nếu thuật toán tốt nhất bạn có thể nghĩ đến về cơ bản là bruteforce, hãy thử một bộ giải thay thế. Chỉ cần liệt kê các ràng buộc của bạn và chạy nó sẽ cho câu trả lời duy nhất của bạn trong vài giây:

278195436
695743128
134628975
549812763
386457291
721369854
913286547
862574319
457931682

Mã được sử dụng (và hình ảnh tham chiếu cho tọa độ):

import z3

letters = "ABCDEFGHI"
numbers = "123456789"
boxes = """
A1 A2 A3
B1 B2 C2 C3
C1 D1 D2
E1 E2 F2
F1 G1
H1 I1
G2 H2 G3 H3 H4
I2 I3 I4
B3 B4 C4
D3 E3 F3
A4 A5 B5
C5 B6 C6
G5 H5 I5 I6
A6 A7
B7 C7
D7 D8 D9
E7 E8 F7 F8
G7 H7
I7 I8
A8 B8 C8
G8 H8
A9 B9 C9
E9 F9
G9 H9 I9
"""
positions = [letter + number
             for letter in letters
             for number in numbers]
S = {pos: z3.Int(pos) for pos in positions}

solver = z3.Solver()

# Every symbol must be a number from 1-9.
for symbol in S.values():
    solver.add(z3.Or([symbol == i for i in range(1, 10)]))

# Every row value must be unique.
for row in numbers:
    solver.add(z3.Distinct([S[col + row] for col in letters]))

# Every column value must be unique.
for col in letters:
    solver.add(z3.Distinct([S[col + row] for row in numbers]))

# Every block must contain every value.
for i in range(3):
    for j in range(3):
        solver.add(z3.Distinct([S[letters[m + i * 3] + numbers[n + j * 3]]
                                for m in range(3)
                                for n in range(3)]))

# Colored boxes.
for box in boxes.split("\n"):
    box = box.strip()
    if not box: continue
    boxsum = z3.Sum([S[pos] for pos in box.split()])
    solver.add(z3.Or([boxsum == 1, boxsum == 4, boxsum == 9,
                      boxsum == 16, boxsum == 25, boxsum == 36]))

# Print solutions.
while solver.check() == z3.sat:
    model = solver.model()
    for row in numbers:
        print("".join(model.evaluate(S[col+row]).as_string()
                    for col in letters))
    print()

    # Prevent next solution from being equivalent.
    solver.add(z3.Or([S[col+row] != model.evaluate(S[col+row])
                      for col in letters
                      for row in numbers]))

Điều đó có vẻ đầy hứa hẹn! Tôi sẽ không bao giờ nghĩ về điều đó. Bạn có tài liệu nào cho việc cài đặt z3 không? Ngoài ra, điều này có nghĩa là chỉ có một giải pháp duy nhất?
đường

1
@balways python -m pip install z3-solvernên lấy Z3. Sau khi chỉnh sửa mã, bây giờ in tất cả các giải pháp thỏa đáng.
orlp

1
@balways Sau khi sửa một lỗi, bây giờ lặp lại tất cả các giải pháp thỏa đáng. Tuy nhiên, nó chỉ tìm thấy một, vì vậy giải pháp là duy nhất.
orlp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.