奇怪的数独验证错误

时间:2015-08-04 18:16:49

标签: python algorithm copy sudoku

我目前正在尝试用Python编写一个递归的数独求解算法。我为Sudokus创建了一个类,它包含一些帮助我操作数独网格的方法。

这是我的代码:

class Sudoku:
    def __init__(self, input, tokens = None):
        self.data = {}
        if tokens is None:
            self.tokens = list(range(1, 10))
        else:
            self.tokens = tokens

        assert len(self.tokens) == 9
        if type(input) == dict:
            self.data = input
        else:
            for i, cell in enumerate(input):
                if cell in self.tokens:
                    self.data[i % 9, i // 9] = cell

    def __repr__(self):
        string = ''
        canvas = [['.'] * 9 for line in range(9)]
        for (col, row), cell in self.data.items():
            canvas[row][col] = str(cell)
        for y, row in enumerate(canvas):
            if not y % 3:
                string += "+-------+-------+-------+\n"
            string += '| {} | {} | {} |\n'.format(' '.join(row[:3]), ' '.join(row[3:6]), ' '.join(row[6:]))
        string += "+-------+-------+-------+"
        return string

    @classmethod
    def sq_coords(cls, cell_x, cell_y):
        #returns all coordinates of cells in the same square as the one in (cell_x, cell_y)
        start_x, start_y = cell_x // 3 * 3, cell_y // 3 * 3
        for dx in range(3):
            for dy in range(3):
                yield (start_x +dx, start_y + dy)

    def copy(self):
        return Sudoku(self.data)

    def clues(self, cell_x, cell_y):
        assert not self.data.get((cell_x, cell_y))
        allowed = set(self.tokens)
        #Remove all numbers on the same row, column and square as the cell
        for row in range(9):
            allowed.discard(self.data.get((cell_x, row)))
        for col in range(9):
            allowed.discard(self.data.get((col, cell_y)))
        for coords in self.sq_coords(cell_x, cell_y):
            allowed.discard(self.data.get(coords))
        return allowed

    def get_all_clues(self):
        clues = {}
        for row in range(9):
                for col in range(9):
                    if not self.data.get((col, row)):
                        clues[col, row] = self.clues(col, row)
        return clues

    def fill_singles(self):
        still_going = True
        did_something = False
        while still_going:
            still_going = False
            for (col, row), clues in self.get_all_clues().items():
                if len(clues) == 1:
                    still_going = True
                    did_something = True
                    self.data[col, row] = clues.pop()
        return did_something

    def place_finding(self):
        still_going = True
        did_something = False
        while still_going:
            still_going = False
            for token in self.tokens:
                for group in self.get_groups():
                    available_spots = [coords for coords, cell in group.items() if cell == None and token in self.clues(*coords)]
                    if len(available_spots) == 1:
                        self.data[available_spots.pop()] = token
                        still_going = True
                        did_something = True
        return did_something

    def fill_obvious(self):
        still_going = True
        while still_going:
            a = self.fill_singles()
            b = self.place_finding()
            still_going = a or b

    def get_groups(self):
        for y in range(9):
            yield {(x, y) : self.data.get((x, y)) for x in range(9)}
        for x in range(9):
            yield {(x, y) : self.data.get((x, y)) for y in range(9)}
        for n in range(9):
            start_x, start_y = n % 3 * 3, n // 3 * 3
            yield {(x, y) : self.data.get((x, y)) for x, y in self.sq_coords(start_x, start_y)}

    def is_valid(self):
        for group in self.get_groups():
            if any([list(group.values()).count(token) > 1 for token in self.tokens]):
                return False
        return True

    def is_solved(self):
        return self.is_valid() and len(self.data) == 9 * 9

def solve(sudoku):
        def loop(su):
            if su.is_solved():
                print(su)
            elif su.is_valid():
                su.fill_obvious()
                print(su)
                for coords, available_tokens in sorted(su.get_all_clues().items(), key=lambda kv: len(kv[1])):
                    for token in available_tokens:
                        new_su = su.copy()
                        new_su.data[coords] = token
                        loop(new_su)
        loop(sudoku)

with open('input.txt') as f:
    numbers = ''
    for i, line in enumerate(f):
        if i >= 9:
            break
        numbers += line.rstrip().ljust(9)

s = Sudoku(numbers, tokens='123456789')

print(s)
solve(s)
print(s)

很抱歉,如果这看起来很乱,但我宁愿给你我所拥有的一切,而不仅仅是一些可能包含或不包含问题的数据。

正如你所看到的,它所做的第一件事是使用fill_singles方法填充Sudoku只有100%确定的数字(填充每个只能用数字x填充的单元格,例如其他8种可能性在其行中,列或块)和place_finding(检查所有标记,看看组中是否只有一个空格 - 行,列或块 - 它们可以适合的位置)。它遍历这两者,直到无法完成任何事情。

然后,它会尝试在空间最小的空间中尝试使用相同的方法解决新制作的网格。这就是我的问题所在。目前,出于调试目的,程序会打印出有效时遇到的网格(组中没有相同的数字两次)。实际上这就是我想要的。但它不能像这样工作;有了这个输入:

+-------+-------+-------+
| . . . | . 2 6 | . . 4 |
| . . . | 7 9 . | 5 . . |
| . . . | . . . | 9 1 . |
+-------+-------+-------+
| . 8 . | 1 . . | . . . |
| 2 3 6 | . . . | 1 8 5 |
| . . . | . . 3 | . 7 . |
+-------+-------+-------+
| . 4 7 | . . . | . . . |
| . . 3 | . 7 8 | . . . |
| 5 . . | 6 3 . | . . . |
+-------+-------+-------+

它输出这样的网格,显然无效:

+-------+-------+-------+
| 1 4 9 | 3 2 6 | 7 3 4 |
| 3 2 4 | 7 9 1 | 5 1 8 |
| 3 7 5 | 3 2 4 | 9 1 2 |
+-------+-------+-------+
| 7 8 6 | 1 1 4 | 3 2 5 |
| 2 3 6 | 9 4 7 | 1 8 5 |
| 4 1 7 | 2 3 3 | 6 7 4 |
+-------+-------+-------+
| 2 4 7 | 1 1 3 | 5 4 6 |
| 1 3 3 | 4 7 8 | 2 5 1 |
| 5 5 4 | 6 3 2 | 1 3 7 |
+-------+-------+-------+

我真的不明白为什么它允许这样的网格通过is_valid测试,特别是当我手动输入上面的网格时,它没有通过测试:

>>> s
+-------+-------+-------+
| 1 4 9 | 3 2 6 | 7 3 4 |
| 3 2 4 | 7 9 1 | 5 1 8 |
| 3 7 5 | 3 2 4 | 9 1 2 |
+-------+-------+-------+
| 7 8 6 | 1 1 4 | 3 2 5 |
| 2 3 6 | 9 4 7 | 1 8 5 |
| 4 1 7 | 2 3 3 | 6 7 4 |
+-------+-------+-------+
| 2 4 7 | 1 1 3 | 5 4 6 |
| 1 3 3 | 4 7 8 | 2 5 1 |
| 5 5 4 | 6 3 2 | 1 3 7 |
+-------+-------+-------+
>>> s.is_valid()
False

任何人都可以在我的代码中看到我没有注意到的错误吗?对不起,我并不是特定的,但是我试着查看我的每一段代码,似乎找不到任何东西。

对于@AnandSKatum:

    26  4
   79 5
      91
 8 1
236   185
     3 7
 47
  3 78
5  63

1 个答案:

答案 0 :(得分:0)

这是我为你写的更好的实现

from numpy.lib.stride_tricks import as_strided
from itertools import chain
import numpy
def block_view(A, block= (3, 3)):
    """Provide a 2D block view to 2D array. No error checking made.
    Therefore meaningful (as implemented) only for blocks strictly
    compatible with the shape of A."""
    # simple shape and strides computations may seem at first strange
    # unless one is able to recognize the 'tuple additions' involved ;-)
    shape= (A.shape[0]/ block[0], A.shape[1]/ block[1])+ block
    strides= (block[0]* A.strides[0], block[1]* A.strides[1])+ A.strides
    return chain.from_iterable(as_strided(A, shape= shape, strides= strides))

def check_board(a):
    """
    a is a 2d 9x9 numpy array, 0 represents None
    """
    for row,col,section in zip(a,a.T,block_view(a,(3,3))):
        s = list(chain.from_iterable(section))
        if any(sum(set(x)) != sum(x) for x in [row,col,s]):
            return False
    return True

a = numpy.array(
    [
        [9,8,7,6,5,4,3,2,1],
        [2,1,5,0,0,0,0,0,0],
        [3,6,4,0,0,0,0,0,0],
        [4,2,0,0,0,0,0,0,0],
        [5,3,0,0,0,0,0,0,0],
        [6,7,0,0,0,0,0,0,0],
        [7,0,0,0,0,0,0,0,0],
        [8,0,0,0,0,0,0,0,0],
        [1,0,0,0,0,0,0,0,0],
    ]
)

print check_board(a)