首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >数独难题解决器

数独难题解决器
EN

Code Review用户
提问于 2015-07-13 17:10:55
回答 2查看 6.1K关注 0票数 7

我已经写了一个程序来解决数独游戏(有趣?)学习Python的方法。它像人类一样解决谜题,使用推理算法,而不是蛮力/回溯,就像我认为那样会是一个更有趣的挑战。

我真的在寻找关于如何减少缩进、代码行或提高效率的任何反馈。我确信我为列/行/子网格重复的函数可以合并成一个,但我只是想不出一种优雅的方法。任何一般性的反馈也将是非常欢迎的,请尽可能多地融入代码中。我是一个二年级的计算机(CS,但低)学生,如果这有任何不同,但可能没有。

我理解这涉及领域的具体知识,即如何解决数独谜题。我只使用以下资源创建了所有算法,程序中包含的所有技术都是为感兴趣的人描述的这里

代码语言:javascript
运行
复制
import itertools

# TERMINOLOGY
# grid:         The main 9x9 grid of the puzzle.
# sub-grid:     A 3x3 sub-grid within the main grid.
# square:       A single square within the grid.
# row:          A whole row of squares within the main grid.
# column:       A whole column of squares within the main grid.
# sub-row:      A row of three squares in a sub-grid.
# sub-column:   A column of three squares in a sub-grid.
# candidate:    A possible solution for a square.
# solved:       A square is solved when the correct value is filled.
#
# For in-depth descriptions of the various sudoku solving techniques 
# used in this program, visit:
# http://www.paulspages.co.uk/sudoku/howtosolve/index.htm
# This is the sole resource used to generate the techniques
# found in this program.


# Holds puzzle itself as 2d list. Blank squares represented as 0.
# Individual square access: main_grid[x][y]
main_grid = [[] for x in range(0, 9)]

# Holds all possible candidates for each square as a 2d list of sets.
# Individual square set access: candidates_grid[x][y]
candidates_grid = [[set() for y in range(0, 9)] for x in range(0, 9)]

# Holds all solved values in an individual row/col/sub-grid
col_set = [set() for x in range(0, 9)]  # Access: col_set[x]
row_set = [set() for x in range(0, 9)]  # Access: row_set[y]
sub_grid_set = [[set() for y in range(0, 3)] for x in range(0, 3)]

# Misc sets used for solving techniques/optimisation
full_set = set({1, 2, 3, 4, 5, 6, 7, 8, 9})
coordinates_set = set({0, 1, 2, 3, 4, 5, 6, 7, 8})


# Populates main_grid, candidates_grid, row/col/block sets from data file
def init():
    puzzle = open('../puzzles/extreme2.txt', 'r')
    for y in range(0, 9):
        next_line = puzzle.readline()
        for x in range(0, 9):
            main_grid[x].append(int(next_line[x]))
            if next_line[x] != '0':
                col_set[x].add(int(next_line[x]))
                row_set[y].add(int(next_line[x]))
                sub_grid_set[int(x / 3)][int(y / 3)].add(int(next_line[x]))

    for y in range(0, 9):
        for x in range(0, 9):
            if main_grid[x][y] == 0:
                candidatesSet = set.union(row_set[y], col_set[x],
                                          sub_grid_set[int(x / 3)][int(y / 3)])
                candidates_grid[x][y] = full_set.difference(candidatesSet)


def iter_over_subgrids(func, *args):
    for sub_grid_y in range(0, 3):
        for sub_grid_x in range(0, 3):
            func(sub_grid_x, sub_grid_y, *args)


def iter_over_line(func, *args):
    for square in range(0, 9):
        func(square, *args)


def print_main_grid():
    for y in range(0, 9):
        for x in range(0, 9):
            print(main_grid[x][y], end="")
            if x % 3 == 2:
                print(" ", end="")
        print("")


def print_candidates_grid():
    for y in range(0, 9):
        for x in range(0, 9):
            print(candidates_grid[x][y], " ", end="")
        print("")


def is_solved():
    for y in range(0, 9):
        if len(row_set[y]) != 9:
            return 0
    return 1


# Writes solution to main_grid, updates sets and tables.
def pencil_in(solution, x, y, func):
    sub_grid_x = int(x / 3)
    sub_grid_y = int(y / 3)
    main_grid[x][y] = solution
    row_set[y].add(solution)
    col_set[x].add(solution)
    sub_grid_set[sub_grid_x][sub_grid_y].add(solution)
    candidates_grid[x][y].clear()

    for sg_y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
        for sg_x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
            candidates_grid[sg_x][sg_y].discard(solution)
    for i in range(0, 9):
        candidates_grid[x][i].discard(solution)
        candidates_grid[i][y].discard(solution)


# Solves squares that have only one candidate.
def single_candidate_square(y):
    for x in range(0, 9):
        if len(candidates_grid[x][y]) == 1:
            pencil_in(candidates_grid[x][y].pop(), x, y,
                      single_candidate_square)


# Solves squares where candidate appears only once in a row.
def single_sq_candidate_row(y):
    for candidate in full_set.difference(row_set[y]):  # Skip solved values
        count = 0
        prev_x = 0
        for x in range(0, 9):
            if candidate in candidates_grid[x][y]:
                count += 1
                prev_x = x
        if count == 1:
            pencil_in(candidate, prev_x, y, single_sq_candidate_row)


# As single_sq_candidate_row, for columns.
def single_sq_candidate_col(x):
    for candidate in full_set.difference(col_set[x]):  # Skip solved values
        count = 0
        prev_y = 0
        for y in range(0, 9):
            if candidate in candidates_grid[x][y]:
                count += 1
                prev_y = y
        if count == 1:
            pencil_in(candidate, x, prev_y, single_sq_candidate_col)


# As single_sq_candidate_row, for subgrids.
def single_sq_candidate_subgrid(sub_grid_x, sub_grid_y):
    for candidate in full_set.difference(sub_grid_set[sub_grid_x][sub_grid_y]):
        count = 0
        prev_coords = [0, 0]
        for y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
            for x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
                if candidate in candidates_grid[x][y]:
                    count += 1
                    prev_coords[0] = x
                    prev_coords[1] = y
        if count == 1:
            pencil_in(candidate, prev_coords[0], prev_coords[1],
                      single_sq_candidate_subgrid)


# Finds candidates in block that lie only on one subrow,
# removes candidates from rest of row.
def number_claiming_row(sub_grid_x, sub_grid_y):
    # Get set of all candidates each subrow
    subrow_sets = [set(), set(), set()]
    for y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
        for x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
            subrow_sets[y % 3] = subrow_sets[y % 3].union(candidates_grid[x][y])

    # Get candidates which only appear in one subrow
    claimed = [subrow_sets[0].difference(subrow_sets[1], subrow_sets[2])]
    claimed.append(subrow_sets[1].difference(subrow_sets[0], subrow_sets[2]))
    claimed.append(subrow_sets[2].difference(subrow_sets[0], subrow_sets[1]))

    # Remove candidates from other subrows in parent row
    for sub_row in range(0, 3):
        for claimant in set(claimed[sub_row]):
            for x in range(0, 9):
                if int(x / 3) != sub_grid_x:
                    candidates_grid[x][sub_grid_y * 3 + sub_row].discard(claimant)


# As number_claiming_row, but for columns
def number_claiming_col(sub_grid_x, sub_grid_y):
    # Get set of all candidates each subcolumn
    subcol_sets = [set(), set(), set()]
    for x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
        for y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
            subcol_sets[x % 3] = subcol_sets[x % 3].union(candidates_grid[x][y])

    # Get candidates which only appear in one subcolumn
    claimed = [subcol_sets[0].difference(subcol_sets[1], subcol_sets[2])]
    claimed.append(subcol_sets[1].difference(subcol_sets[0], subcol_sets[2]))
    claimed.append(subcol_sets[2].difference(subcol_sets[0], subcol_sets[1]))

    # Remove candidates from other subcolumns in parent column
    for sub_col in range(0, 3):
        for claimant in set(claimed[sub_col]):
            for y in range(0, 9):
                if int(y / 3) != sub_grid_y:
                    candidates_grid[sub_grid_x * 3 + sub_col][y].discard(claimant)


# Finds sets of n squares in a row where:
# - No squares contain more than n candidates each.
# - The cardinality of the set of all the candidates in squares is n.
# All candidates in that set can be assumed to lie in those squares,
# so the set of candidates can be removed from all other squares in
# that row. Sudoku solvers may already know disjoint subsets as "pairs"
# or "triples".
#
# Basic example: three squares in a row contain the candidate sets
# {2,4}, {2,7} and {4,7} respectively. All three squares contain no
# more than three candidates, and the set of all candidates is {2,4,7},
# which has a cardinality of three. It can then be assumed that those
# squares MUST contain 2, 4 and 7 and nothing else. Any squares outside
# those three in the row can then have the candidates 2, 4 and 7 removed.
def disjoint_subsets_row(y, n):
    sets = []
    # Get all candidate sets in row with cardinality no greater than n
    for x in range(0, 9):
        if 1 < len(candidates_grid[x][y]) <= n:
            sets.append(candidates_grid[x][y])

    # For all disjoint subsets found, remove candidates from other squares
    for d in get_disjoint_subsets(sets, n):
        for x in range(0, 9):
            if not candidates_grid[x][y].issubset(d):
                candidates_grid[x][y] = candidates_grid[x][y].difference(d)


# As disjoint_subsets_row, for columns
def disjoint_subsets_col(x, n):
    sets = []
    # Get all candidate sets in row with cardinality no greater than n
    for y in range(0, 9):
        if 1 < len(candidates_grid[x][y]) <= n:
            sets.append(candidates_grid[x][y])

    # For all disjoint subsets found, remove candidates from other squares
    for d in get_disjoint_subsets(sets, n):
        for y in range(0, 9):
            if not candidates_grid[x][y].issubset(d):
                candidates_grid[x][y] = candidates_grid[x][y].difference(d)


# As disjoint_subsets_row, for sub-grids.
def disjoint_subsets_subgrid(sub_grid_x, sub_grid_y, n):
    sets = []
    # Get all candidate sets in row with cardinality no greater than n
    for y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
        for x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
            if 1 < len(candidates_grid[x][y]) <= n:
                sets.append(candidates_grid[x][y])

    # For all disjoint subsets found, remove candidates from other squares
    for d in get_disjoint_subsets(sets, n):
        for y in range(sub_grid_y * 3, sub_grid_y * 3 + 3):
            for x in range(sub_grid_x * 3, sub_grid_x * 3 + 3):
                if not candidates_grid[x][y].issubset(d):
                    candidates_grid[x][y] = candidates_grid[x][y].difference(d)


def get_disjoint_subsets(sets, n):
    disjoint_subsets = set()
    # For each combination of n sets in sets
    for combination in itertools.combinations(sets, n):
        superset = set()
        # For each individual set in combination
        for c in combination:
            superset = superset.union(c)
        if len(superset) == n:
            # Cardinality of candidate superset in combination is n, is djss.
            disjoint_subsets.add(frozenset(superset))
    return disjoint_subsets


# Runs through solving techniques until puzzle solved or no possible solution.
def solve():
    for x in range(0, 100):
        iter_over_line(single_candidate_square)
        iter_over_line(single_sq_candidate_row)
        iter_over_line(single_sq_candidate_col)
        iter_over_subgrids(single_sq_candidate_subgrid)
        iter_over_subgrids(number_claiming_row)
        iter_over_subgrids(number_claiming_col)
        for n in range(2, 5):
            iter_over_line(disjoint_subsets_row, n)
            iter_over_line(disjoint_subsets_col, n)
            iter_over_subgrids(disjoint_subsets_subgrid, n)
        if is_solved() == 1:
            print_main_grid()
            break

init()
solve()

在文本文件中解题的格式如下:

代码语言:javascript
运行
复制
400700900  
000004000  
070280006  
500000010  
301060408  
020000009  
900036020  
000800000  
004001003  

数独游戏的例子:

代码语言:javascript
运行
复制
4 - - | 7 - - | 9 - -  
- - - | - - 4 | - - -  
- 7 - | 2 8 - | - - 6
------|-------|------  
5 - - | - - - | - 1 -  
3 - 1 | - 6 - | 4 - 8  
- 2 - | - - - | - - 9
------|-------|------  
9 - - | - 3 6 | - 2 -  
- - - | 8 - - | - - -  
- - 4 | - - 1 | - - 3 

以上是一个“极端”水平的谜题。

EN

回答 2

Code Review用户

回答已采纳

发布于 2015-07-13 17:30:29

使用文件句柄

别这么做:

益智=打开(‘./谜题/极值2.txt’,'r')

使用with代替:

代码语言:javascript
运行
复制
with open('../puzzles/extreme2.txt') as puzzle:
    # ...

在您的版本中,您忘记关闭文件句柄。这是个糟糕的做法。使用with时,文件句柄将自动关闭。

使用booleans

而不是返回0表示失败,返回1表示成功:

def is_solved():y在范围(0,9):if len(row_set是) != 9:返回0返回1

使用布尔人:

代码语言:javascript
运行
复制
def is_solved():
    for y in range(0, 9):
        if len(row_set[y]) != 9:
            return False
    return True

可用性

如果拼图文件是命令行参数,而不是硬编码路径../puzzles/extreme2.txt,那就太好了。

Documentation

很高兴你包括了关于术语的解释。但最好使用适当的文档字符串。

Simplifications

您可以替换这个:

full_set = set({1,2,3,4,5,6,7,8,9})

具有集合文字:

代码语言:javascript
运行
复制
full_set = {1, 2, 3, 4, 5, 6, 7, 8, 9}

您可以编写range(9)而不是range(0, 9)

与其使用int(x / 3),不如使用x // 3

票数 7
EN

Code Review用户

发布于 2015-07-13 18:55:34

你可以通过使用列表理解来使它更紧凑。下面的函数将执行所有的拼图检查,并返回给定特定谜题的特定行和列的所有可能的移动,尽管我使用的是字符而不是int(您可以通过将该部分转换为m+1 for m in range(9))。

代码语言:javascript
运行
复制
def puzzle_moves(row,col,puzzle):
    same_row = {puzzle[row][c] for c in range(9)}
    same_col = {puzzle[r][col] for r in range(9)}
    # box row/col refer to the row/col of the box that row/col is in
    box_row = 3*(row/3)
    box_col = 3*(col/3)
    same_box = {puzzle[box_row+r][box_col+c] for r in range(3) for c in range(3)}
    return {m for m in '123456789'
        if m not in same_row
        and m not in same_col
        and m not in same_box}

我的解决方案可以在几秒钟内解决大量的“极端”谜题,我认为它很容易读懂(YMMV!)

编辑:我想这不像我想的那么清楚!:

要理解这一点,请查看返回语句:我们希望返回不在同一行、同一列或同一框中的任何可能的“移动”。返回语句之前的所有行都定义了在同一行/col/box中的含义。

让我们从same_row开始作为一个示例:

代码语言:javascript
运行
复制
same_row = {puzzle[row][c] for c in range(9)}

我们通过拼图,行和列,我们想知道哪些移动是在这一行的拼图。例如,假设益智是一个数独拼图,如下所示:

如果我们传入第0行和第0行,我们期望同一行返回第0(第一)行中的所有内容:{2,7,8,9}

要自己尝试使用发布的拼图,请打开python提示符并键入puzzle_str=""",然后粘贴您的拼图,然后再添加"""

代码语言:javascript
运行
复制
>>>puzzle_str = """400700900...etc..."""

然后,您可以将该拼图构造为列表列表:

代码语言:javascript
运行
复制
puzzle = [[c for c in line.strip()] for line in puzzle_str.strip().split('\n')]

该行将拼图字符串拆分为换行符(在结束或开始删除任何附加字符之后),然后返回每个字符。

然后,您可以通过传递包含任意行的益智函数和上面的拼图来调用上面的函数。您也可以尝试每一行代码,所以让我们再看一遍same_row

代码语言:javascript
运行
复制
row = 0
col = 0
{puzzle[row][c] for c in range(9)}  # same_row, from above function

因此,这将返回{'0','4','7','9'},这是您的拼图第一行中的不同字符。same_col遵循同样的逻辑。

获得same_box的难度更大。要理解这一部分,请将谜题想象为有3个“框行”和3个“框列”,它们定义了9个框。要获得方框行/科尔,您可以使用传入的行/col,并将其除以3,这将截断它(因为它是一个int)。若要查看此操作,请查看以下内容:

代码语言:javascript
运行
复制
[i/3 for i in range(9)]
[0, 0, 0, 1, 1, 1, 2, 2, 2]

一旦确定了box_rowbox_colsame_box表达式就与same_rowsame_col的逻辑完全相同。for r in range(3) for c in range(3)为您提供了3x3盒的所有组合。

我已经闲逛了,enough...please,让我知道是否还有另一个特定的部分我可以解释?

编辑#2好的,这是另一个版本,仍然相当简洁,但运行速度更快(大约10% )。这一次是一个面向对象的设计,这不是我的典型追求,但嘿,至少它是漂亮的!

代码语言:javascript
运行
复制
import copy

possible_moves = set([m for m in '123456789'])

class Sudoku(object):
    def __init__(self, puzzle_str):
        self.rows = [set() for r in range(9)]
        self.cols = [set() for c in range(9)]
        self.boxes = [[set() for bc in range(3)] for br in range(3)]
        self.grid = [[c for c in line.strip()] for line in puzzle_str.strip().split('\n')]
        for r, row in enumerate(self.grid):
            for c, m in enumerate(row):
                self.move(r, c, m)

    def move(self, r, c, m):
        self.rows[r].add(m)
        self.cols[c].add(m)
        self.boxes[int(r/3)][int(c/3)].add(m)
        self.grid[r][c] = m

    def moves(self, r, c):
        return possible_moves - self.rows[r] - self.cols[c] - self.boxes[int(r/3)][int(c/3)]

    def __str__(self):
        return '\n'.join(['.'.join([c for c in line]) for line in self.grid])

worst_best_move = ((10),(),())

def solve_sudoku(puzzle):
    best_move = worst_best_move
    found_move = True
    finished = True

    while found_move:
        finished = True
        found_move = False
        best_move = worst_best_move
        for r in range(0,9):
            for c in range(0,9):
                if puzzle.grid[r][c]=='0':
                    possible_moves = puzzle.moves(r,c)
                    num_moves = len(possible_moves)
                    if num_moves==0:
                        return None
                    if num_moves==1:
                        found_move=True
                        puzzle.move(r, c, possible_moves.pop())
                    else:
                        finished = False
                        if num_moves < best_move[0]:
                            best_move =(num_moves,(r,c), possible_moves)
    if finished:
        return puzzle

    target = best_move[1]
    for move in best_move[2]:
        guess_puzzle = copy.deepcopy(puzzle)
        guess_puzzle.move(target[0], target[1], move)
        result = solve_sudoku(guess_puzzle)
        if result is not None:
            return result
    return None

def run_sudoku_test():
    puzzle_str = """
400700900
000004000
070280006
500000010
301060408
020000009
900036020
000800000
004001003"""
    puzzle = Sudoku(puzzle_str)
    #print(str(puzzle))
    result = solve_sudoku(puzzle)
    if result is None:
        print 'Failed to solve %s'%puzzle_num
    else:
        pass#print_sudoku_puzzle(result)


if __name__=='__main__':
    import timeit
    print(timeit.timeit('run_sudoku_test()',setup='from __main__ import run_sudoku_test',number=50))
票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/96792

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档