简体   繁体   English

数独谜题与包含平方数的盒子

[英]Sudoku Puzzle with boxes containing square numbers

Two days ago, I was given a sudoku problem that I tried to solve with Python 3. I've been informed that a solution does exist, but I'm not certain if there exists multiple solutions.两天前,我收到了一个我试图用 Python 3 解决的数独问题。我被告知确实存在一个解决方案,但我不确定是否存在多个解决方案。

The problem is as following: A 9x9 grid of sudoku is completely empty.问题如下: 一个 9x9 的数独网格是完全空的。 It does however contain colored boxes , and inside these boxes, the sum of the numbers has to be a square number .然而,它确实包含彩色框,并且在这些框内,数字的总和必须是平方数 Other than that, normal sudoku rules apply.除此之外,正常的数独规则适用。

The issue here is not solving a sudoku puzzle, but rather generating a viable puzzle, that satisfies the rules of the colored boxes .这里的问题不是解决数独谜题,而是生成一个满足彩色框规则的可行谜题。

My strategy我的策略

Using numpy arrays, I have divided the grid into 81 indices, which can be rearranged to a 9x9 grid.使用 numpy 数组,我将网格划分为 81 个索引,这些索引可以重新排列为 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]]

Here is a list containing all the blocks of indices.这是一个包含所有索引块的列表。

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]]

As you can see from the picture , or from the array above, the boxes are arranged into blocks of 2, 3, 4, or 5 (8 twos, 12 threes, 3 fours, 1 fiver).正如您从图片或上面的数组中看到的,这些盒子被排列成 2、3、4 或 5 个块(8 个二、12 个三、3 个四、1 个五)。 I've also noticed that a box can contain multiple numbers without breaking any rules of sudoku, but only 2 of one number is possible.我还注意到一个盒子可以包含多个数字而不会违反任何数独规则,但一个数字中只能有 2 个。 Given that information, the biggest possible square would be 36, as 9+9+8+7+6 = 39, and thus no sum of a block could ever reach 49. To find out if the sum of a list contains a square number, I've made the following function:鉴于这些信息,最大可能的平方将是 36,因为 9+9+8+7+6 = 39,因此一个块的总和永远不会达到 49。 找出列表的总和是否包含一个平方数,我做了以下功能:

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

To find out if a list contain the correct amount of duplicates, that is, more than one duplicate of only one number, I've made the following function:为了找出列表是否包含正确数量的重复项,即只有一个数字的多个重复项,我做了以下功能:

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

Now, given the digits 1-9, there are limited ways solutions to a list, if the list has to sum into a square number.现在,给定数字 1-9,如果列表必须求和成一个平方数,那么列表的解决方法是有限的。 Using itertools , I could find the solutions, dividing them into an array, where index 0 contains blocks of twos, index 1 contains blocks of threes, and so on.使用itertools ,我可以找到解决方案,将它们分成一个数组,其中索引 0 包含两个块,索引 1 包含三个块,依此类推。

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)])

However, any permutation of these lists are viable solutions to the "square problem".然而,这些列表的任何排列都是“平方问题”的可行解决方案。 Using itertools again, the total amount of possible boxes (without the sudoku rules) sums to 8782.再次使用itertools ,可能的盒子总数(没有数独规则)总和为 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

This should be enough to implement functionality that decides if a board is legal, that is, rows, columns and boxes only contains one each of the digits 1-9.这应该足以实现决定棋盘是否合法的功能,即行、列和框仅包含 1-9 位数字中的每一个。 My implementation:我的实现:

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))

Difficulties with runtime运行时的困难

A straightforward approach would be to check every single combination of every single block.一种直接的方法是检查每个块的每个组合。 I have dones this, and produced several viable problems, however the complexity of my algorithm makes this take far too long time.我已经这样做了,并产生了几个可行的问题,但是我的算法的复杂性使得这需要很长时间。

Instead, I tried to randomize some of the properties: The order of the blocks and the order of the solutions.相反,我试图随机化一些属性:块的顺序和解决方案的顺序。 Using this, I limited the number of tries, and checked if a solution was viable:使用这个,我限制了尝试的次数,并检查了一个解决方案是否可行:

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)

In the code above, the variable score refers to how many blocks the algorithm could find during an attempt.在上面的代码中,变量 score 是指算法在尝试期间可以找到多少块。 The variable correct refers to how many of the generated sudoku boards could be completed.变量正确是指可以完成多少生成的数独板。 If you are interested in how well it did in 700 attempts, here are some stats (This is a historgram, the x-axis represents the scores, and the y-axis represents how many of each score was present during these 700 attempts).如果您对它在 700 次尝试中的表现感兴趣,这里有一些统计数据(这是一个直方图,x 轴代表分数,y 轴代表在这 700 次尝试中每个分数出现的数量)。

What I need help with我需要什么帮助

I am struggling to find a feasible way to find a solution to this problem, that can actually run in a finite amount of time.我正在努力寻找一种可行的方法来解决这个问题,它实际上可以在有限的时间内运行。 I would greatly appreciate any tips regarding making some of my code faster or better, any ideas of a different approach to the problem, any solutions to the problem, or some useful tips about Python/Numpy relevant to this problem.我将不胜感激有关使我的某些代码更快或更好的任何提示,对问题的不同方法的任何想法,问题的任何解决方案,或有关与此问题相关的 Python/Numpy 的一些有用提示。

This is where I would use a SMT solver.这是我将使用 SMT 求解器的地方。 They are a lot more powerful than people give credit for.他们比人们认为的要强大得多。 If the best algorithm you can think of is essentially bruteforce, try a solver instead.如果您能想到的最佳算法本质上是蛮力,请尝试使用求解器。 Simply listing your constraints and running it gives your unique answer in a couple seconds:只需列出您的约束并运行它,几秒钟内就会给出您独特的答案:

278195436
695743128
134628975
549812763
386457291
721369854
913286547
862574319
457931682

The code used (and reference image for coordinates):使用的代码(和坐标参考图像):

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]))

A list called Boxes is created with 9 elements, each being another list.创建了一个名为 Boxes 的列表,其中包含 9 个元素,每个元素都是另一个列表。 These 9 lists correspond to each of the 9 boxes, and each of the lists contains tuples as the elements with the row and column indices for each square in that box.这 9 个列表对应于 9 个盒子中的每一个,每个列表都包含元组作为元素,该元素具有该盒子中每个方块的行和列索引。 Explicitly entering the values in a similar way to the following would have had the same effect (but would have been a waste of time):以与以下类似的方式显式输入值会产生相同的效果(但会浪费时间):

 # The boxes list is created, with the row and column index of each square in each box Boxes = [ [(3*i+k+1, 3*j+l+1) for k in range(3) for l in range(3)] for i in range(3) for j in range(3) ]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM