数独求解算法
by Ali Spittel
通过Ali Spittel
This article will be part technical, part personal story, and part cultural critique. If you are just here for the code and explanation, jump to the The Initial Approach header!
本文将部分是技术性的,一部分是个人故事,一部分是文化评论。 如果仅在此处提供代码和说明,请跳至“初始方法”标题!
This story starts a few years ago in a college computer science classroom. I had an untraditional path to writing code — I randomly enrolled in a computer science class during my sophomore year of college, because I had an extra credit hour and I was curious what it was about. I thought we would learn how to use Microsoft Word and Excel — I genuinely had no idea what code was.
这个故事是几年前在大学计算机科学教室开始的。 我在编写代码方面有一条非传统的途径-在大学二年级时,我随机参加了计算机科学课程,因为我有一个额外的学分时间,并且很好奇它的用途。 我以为我们会学习如何使用Microsoft Word和Excel-我真的不知道什么是代码。
My high school definitely did not have any coding classes, they barely had functioning computers! I didn’t play video games or engage in activities that traditionally lead to kids learning how to code, either. So coding was brand new to me when I took that Python class in college.
我的高中绝对没有任何编码课,他们几乎没有运转的电脑! 我也没有玩视频游戏或从事传统上导致孩子学习如何编码的活动。 因此,当我上大学的Python课时,编码对我来说是全新的。
As soon as I walked into the classroom, they had us type Python code into Idle, a text editor that comes with the Python language. They had printed the code and just had us type it in and run it — I was immediately hooked. Over the course of that class, I built a Tic Tac Toe script with a GUI to input pieces and a Flappy Bird clone. It honestly came pretty easily to me, and I had a ton of fun. I quickly decided to minor in computer science, and I just wanted to write more code.
我走进教室后,他们让我们在Python附带的文本编辑器Idle中输入Python代码。 他们已经打印了代码,只是让我们输入并运行它-我立即被迷住了。 在该课程的整个过程中,我构建了一个Tic Tac Toe脚本,该脚本带有一个GUI,用于输入片段和Flappy Bird克隆。 老实说,这对我来说很轻松,我玩得很开心。 我很快决定学习计算机科学,并只想编写更多代码。
The next semester, I enrolled in a Data Structures and Algorithms course which was next in the computer science sequence. The class was taught in C++, which, unbeknownst to me, was supposed to be learned over the summer before the class. It quickly became obvious that the professors were trying to use the class to filter out students — around 50% of the enrollees on day one made it through the semester. We even changed classrooms from a lecture hall to a break out room. My pride was the only thing keeping me in the class. I felt completely lost in pretty much every lesson. I spent many all-nighters working on projects and studying for the exams.
下学期,我参加了计算机结构课程中的下一个数据结构和算法课程。 这门课是用C ++讲授的,这对我来说并不为人所知,应该在课前的夏天学习。 很快就很明显,教授们正试图利用课堂过滤掉学生-在第一学期入学的大约50%的学生。 我们甚至将教室从演讲厅更改为分组讨论室。 我的骄傲是唯一让我上课的东西。 我几乎在每节课中都完全迷失了。 我花了许多通宵工作,从事项目研究和考试。
One problem in particular really got me — we were supposed to build a program in C++ that would solve any Sudoku problem. Again, I spent countless hours on the assignment trying to get the code working. By the time the project was due, my solution worked for some of the test cases but not all of them. I ended up getting a C+ on my assignment — one of my worst grades in all of college.
特别是有一个真正的问题困扰我-我们应该用C ++构建一个程序,该程序可以解决任何Sudoku问题。 再次,我花了无数小时来完成代码,以使代码正常工作。 到项目到期时,我的解决方案适用于一些测试用例,但不是全部。 我的作业最终得到了C +,这是我整个大学里最差的成绩之一。
After that semester, I abandoned my idea of minoring in computer science, completely quit coding, and stuck to what I thought I was good at — writing and politics.
在那个学期之后,我放弃了计算机科学辅修的想法,完全放弃了编码,并坚持自己认为自己擅长的东西-写作和政治。
Of course, funny things happen in life and I obviously started coding again, but it took me a long time to feel like I was a competent programmer.
当然,生活中会发生有趣的事情,很显然,我再次开始编写代码,但是花了我很长时间才觉得自己是一位能干的程序员。
All that being said, a few years later into my programming journey, I decided to retry implementing the Sudoku solving algorithm to prove to myself that I could implement it now. The code isn’t perfect, but it will solve pretty much any Sudoku puzzle. Let’s walk through the algorithm and then the implementation.
话虽这么说,几年后,在我的编程旅程中,我决定重试实现Sudoku求解算法,以向自己证明我现在可以实现它。 该代码不是完美的,但几乎可以解决任何数独难题。 让我们逐步介绍一下算法,然后执行一下。
In case you haven’t played Sudoku puzzles before, they are number puzzles in which each row, column, and 3x3 square in the puzzle must have the numbers 1–9 represented exactly once. There are lots of approaches to solving these puzzles, many of which can be replicated by a computer instead of a person. Usually, when we solve them using a computer, we will use nested arrays to represent the Sudoku board like so:
如果您以前从未玩过Sudoku拼图,则它们是数字拼图,其中拼图中的每一行,每一列和3x3正方形必须将数字1–9精确地表示一次。 解决这些难题的方法很多,其中许多方法可以由计算机代替人来复制。 通常,当我们使用计算机求解它们时,我们将使用嵌套数组来表示Sudoku板,如下所示:
puzzle = [[5, 3, 0, 0, 7, 0, 0, 0, 0], [6, 0, 0, 1, 9, 5, 0, 0, 0], [0, 9, 8, 0, 0, 0, 0, 6, 0], [8, 0, 0, 0, 6, 0, 0, 0, 3], [4, 0, 0, 8, 0, 3, 0, 0, 1], [7, 0, 0, 0, 2, 0, 0, 0, 6], [0, 6, 0, 0, 0, 0, 2, 8, 0], [0, 0, 0, 4, 1, 9, 0, 0, 5], [0, 0, 0, 0, 8, 0, 0, 7, 9]]
When solved, the zeros will be filled in with actual numbers:
求解后,零将用实际数字填充:
solution = [[5, 3, 4, 6, 7, 8, 9, 1, 2], [6, 7, 2, 1, 9, 5, 3, 4, 8], [1, 9, 8, 3, 4, 2, 5, 6, 7], [8, 5, 9, 7, 6, 1, 4, 2, 3], [4, 2, 6, 8, 5, 3, 7, 9, 1], [7, 1, 3, 9, 2, 4, 8, 5, 6], [9, 6, 1, 5, 3, 7, 2, 8, 4], [2, 8, 7, 4, 1, 9, 6, 3, 5], [3, 4, 5, 2, 8, 6, 1, 7, 9]]
Because I didn’t feel like writing a full test suite with different puzzles, I used the challenges on CodeWars to test myself. The first problem I tried was this — where all of the puzzles were “easy” Sudokus that could be solved without a more complex algorithm.
因为我不想编写包含不同难题的完整测试套件,所以我使用CodeWars上的挑战来测试自己。 我想第一个问题是这个 -所有的谜题是“容易”的数独,可能没有更复杂的算法来解决。
I decided to try and solve the Sudokus in the way I personally do — where I would find the possible numbers for a space, keep track of them, and if there is only one possible number, plug it into that spot. Since these were easier Sudokus, this approach worked fine for this Kata, and I passed.
我决定尝试以我个人的方式解决数独问题-在那儿我会找到一个空间的可能数字,并对其进行跟踪,如果只有一个可能的数字,则将其插入该位置。 由于这些方法比Sudokus更容易使用,因此此方法对于此Kata效果很好,我通过了。
Here’s my (uncleaned) code!
这是我的(未清理的)代码!
class SudokuSolver: def __init__(self, puzzle): self.puzzle = puzzle self.box_size = 3
def find_possibilities(self, row_number, column_number): possibilities = set(range(1, 10)) row = self.get_row(row_number) column = self.get_column(column_number) box = self.get_box(row_number, column_number) for item in row + column + box: if not isinstance(item, list)and item in possibilities: possibilities.remove(item) return possibilities
def get_row(self, row_number): return self.puzzle[row_number]
def get_column(self, column_number): return [row[column_number] for row in self.puzzle]
def get_box(self, row_number, column_number): start_y = column_number // 3 * 3 start_x = row_number // 3 * 3 if start_x < 0: start_x = 0 if start_y < 0: start_y = 0 box = [] for i in range(start_x, self.box_size + start_x): box.extend(self.puzzle[i][start_y:start_y+self.box_size]) return box
def find_spot(self): unsolved = True while unsolved: unsolved = False for row_number, row in enumerate(self.puzzle): for column_number, item in enumerate(row): if item == 0: unsolved = True possibilities = self.find_possibilities( row_number, column_number) if len(possibilities) == 1: self.puzzle[row_number][column_number] = list(possibilities)[ 0] return self.puzzle
def sudoku(puzzle): sudoku = SudokuSolver(puzzle) return sudoku.find_spot()
Of course, I also wanted to solve more difficult Sudoku puzzles, so I decided to implement a more complex algorithm in order to solve those puzzles.
当然,我也想解决更困难的Sudoku难题,因此我决定实施一种更复杂的算法来解决这些难题。
One algorithm to solve Sudoku puzzles is the backtracking algorithm. Essentially, you keep trying numbers in empty spots until there aren’t any that are possible, then you backtrack and try different numbers in the previous slots.
解决数独难题的一种算法是回溯算法。 本质上,您一直在空白处尝试数字,直到没有可能为止,然后回溯并在前一个插槽中尝试其他数字。
The first thing that I did was continue my “easy” Sudoku solver’s approach of finding the possible values for each square based on which values were already in that square’s row, column, and box. I stored all of these values in a list so that I could quickly refer to them while backtracking or finding which value to use in that square.
我要做的第一件事是继续我的“简单” Sudoku求解器的方法,根据该正方形的行,列和框中已存在的值来查找每个正方形的可能值。 我将所有这些值存储在一个列表中,以便在回溯或找到要在该正方形中使用的值时快速引用它们。
Next, I needed to implement the forward moving and backtracking of putting items in each space. I put markers on each non-given space (so the ones that were zeros when the game started) so that those spaces would be included in the backtracking and given spots wouldn’t be. I then iterated through those un-solved spots. I would put the first item of the possible value list in that spot and then move to the next unsolved spot. I would then put the first possible value of that spot in its place. If it conflicted with the value of the previous slot, I would then move to the second item in the list of possible values and then move to the next slot.
接下来,我需要在每个空间中实现放置物品的前进和后退。 我在每个非给定的空间(游戏开始时为零)上放置了标记,以便将这些空间包括在回溯中,而不会出现在给定的位置上。 然后,我遍历那些未解决的地方。 我将可能值列表的第一项放在该位置,然后移至下一个未解决的位置。 然后,我将那个位置的第一个可能的值放在它的位置。 如果它与前一个插槽的值冲突,那么我将移至可能值列表中的第二项,然后移至下一个插槽。
That process would continue until there was no possible move for a given spot — that is, the end of the possible value list was reached and none of the values worked in that row, column, or box. Then, the backtracking algorithm kicked in.
该过程将一直持续到给定位置没有任何可能的移动为止-也就是说,到达可能值列表的末尾,并且该行,列或框中的任何值均不起作用。 然后,回溯算法开始了。
Within the backtracking implementation, the code would move back to the last spot that was filled in and move to the next possible value and then start moving forward again. If the last of the possible values was reached at that spot as well, the backtracking algorithm would keep moving backwards until there was a spot that could be incremented.
在回溯实现中,代码将移回填充的最后一个点,并移至下一个可能的值,然后再次开始向前移动。 如果在那个位置也达到了最后一个可能的值,则回溯算法将继续向后移动,直到有一个可以增加的位置为止。
Once the end of the puzzle was reached with correct values in each square, the puzzle was solved!
一旦拼图的结尾达到每个正方形的正确值,拼图便解决了!
I like object oriented approaches, so I have two different classes in my solution: one for the cell and one for the Sudoku board. My very imperfect code looks like this:
我喜欢面向对象的方法,因此解决方案中有两个不同的类:一个用于单元,一个用于Sudoku板。 我非常不完善的代码如下所示:
class Cell: """One individual cell on the Sudoku board"""
def __init__(self, column_number, row_number, number, game): # Whether or not to include the cell in the backtracking self.solved = True if number > 0 else False self.number = number # the current value of the cell # Which numbers the cell could potentially be self.possibilities = set(range(1, 10)) if not self.solved else [] self.row = row_number # the index of the row the cell is in self.column = column_number # the index of the column the cell is in self.current_index = 0 # the index of the current possibility self.game = game # the sudoku game the cell belongs to if not self.solved: # runs the possibility checker self.find_possibilities()
def check_area(self, area): """Checks to see if the cell's current value is a valid sudoku move""" values = [item for item in area if item != 0] return len(values) == len(set(values))
def set_number(self): """changes the number attribute and also changes the cell's value in the larger puzzle""" if not self.solved: self.number = self.possibilities[self.current_index] self.game.puzzle[self.row][self.column] = self.possibilities[self.current_index]
def handle_one_possibility(self): """If the cell only has one possibility, set the cell to that value and mark it as solved""" if len(self.possibilities) == 1: self.solved = True self.set_number()
def find_possibilities(self): """filter the possible values for the cell""" for item in self.game.get_row(self.row) + self.game.get_column(self.column) + self.game.get_box(self.row, self.column): if not isinstance(item, list) and item in self.possibilities: self.possibilities.remove(item) self.possibilities = list(self.possibilities) self.handle_one_possibility()
def is_valid(self): """checks to see if the current number is valid in its row, column, and box""" for unit in [self.game.get_row(self.row), self.game.get_column(self.column), self.game.get_box(self.row, self.column)]: if not self.check_area(unit): return False return True
def increment_value(self): """move number to the next possibility while the current number is invalid and there are possibilities left""" while not self.is_valid() and self.current_index < len(self.possibilities) - 1: self.current_index += 1 self.set_number()
class SudokuSolver: """contains logic for solving a sudoku puzzle -- even very difficult ones using a backtracking algorithm"""
def __init__(self, puzzle): self.puzzle = puzzle # the 2d list of spots on the board self.solve_puzzle = [] # 1d list of the Cell objects # the size of the boxes within the puzzle -- 3 for a typical puzzle self.box_size = int(len(self.puzzle) ** .5) self.backtrack_coord = 0 # what index the backtracking is currently at
def get_row(self, row_number): """Get the full row from the puzzle based on the row index""" return self.puzzle[row_number]
def get_column(self, column_number): """Get the full column""" return [row[column_number] for row in self.puzzle]
def find_box_start(self, coordinate): """Get the start coordinate for the small sudoku box""" return coordinate // self.box_size * self.box_size
def get_box_coordinates(self, row_number, column_number): """Get the numbers of the small sudoku box""" return self.find_box_start(column_number), self.find_box_start(row_number)
def get_box(self, row_number, column_number): """Get the small sudoku box for an x and y coordinate""" start_y, start_x = self.get_box_coordinates(row_number, column_number) box = [] for i in range(start_x, self.box_size + start_x): box.extend(self.puzzle[i][start_y:start_y+self.box_size]) return box
def initialize_board(self): """create the Cells for each item in the puzzle and get its possibilities""" for row_number, row in enumerate(self.puzzle): for column_number, item in enumerate(row): self.solve_puzzle.append( Cell(column_number, row_number, item, self))
def move_forward(self): """Move forwards to the next cell""" while self.backtrack_coord < len(self.solve_puzzle) - 1 and self.solve_puzzle[self.backtrack_coord].solved: self.backtrack_coord += 1
def backtrack(self): """Move forwards to the next cell""" self.backtrack_coord -= 1 while self.solve_puzzle[self.backtrack_coord].solved: self.backtrack_coord -= 1
def set_cell(self): """Set the current cell to work on""" cell = self.solve_puzzle[self.backtrack_coord] cell.set_number() return cell
def reset_cell(self, cell): """set a cell back to zero""" cell.current_index = 0 cell.number = 0 self.puzzle[cell.row][cell.column] = 0
def decrement_cell(self, cell): """runs the backtracking algorithm""" while cell.current_index == len(cell.possibilities) - 1: self.reset_cell(cell) self.backtrack() cell = self.solve_puzzle[self.backtrack_coord] cell.current_index += 1
def change_cells(self, cell): """move forwards or backwards based on the validity of a cell""" if cell.is_valid(): self.backtrack_coord += 1 else: self.decrement_cell(cell)
def solve(self): """run the other functions necessary for solving the sudoku puzzle""" self.move_forward() cell = self.set_cell() cell.increment_value() self.change_cells(cell)
def run_solve(self): """runs the solver until we are at the end of the puzzle""" while self.backtrack_coord <= len(self.solve_puzzle) - 1: self.solve()
def solve(puzzle): solver = SudokuSolver(puzzle) solver.initialize_board() solver.run_solve() return solver.puzzle
Hard Sudoku Solver
硬数独解算器
Sometimes it just takes time and practice. The Sudoku solver I spent countless college hours on took me less than an hour a few years later.
有时,这需要时间和练习。 数年后,我花了数小时的大学学习的数独求解器使我在不到一年的时间内就花了不到一个小时。
I will say that computer science programs don’t tend to start in a way that allows people who didn’t write code earlier in life to participate. In a few years, computer science education policies may change. But for now, this eliminates people who grew up in small towns, who weren’t interested in coding growing up, or who went to weaker high schools.
我要说的是,计算机科学程序的启动方式往往不会允许那些没有在生命早期编写代码的人参与。 几年后,计算机科学教育政策可能会发生变化。 但是现在,这消除了在小镇上长大的人,对编码的成长不感兴趣的人,或者去了较弱的中学的人。
In part, this definitely contributes to the success of coding bootcamps which start with the fundamentals and teach the less conceptual web development skills rather than heavy algorithms.
在某种程度上,这无疑有助于编码训练营的成功,该训练营从基础开始,并教授较少概念的Web开发技能,而不是繁琐的算法。
I can now write the Sudoku solving algorithm, but I don’t think it’s a necessary skill for developers to have — I still became a successful software engineer shortly after that time when I couldn’t implement the Sudoku solver.
我现在可以编写Sudoku求解算法,但是我认为这不是开发人员必须具备的技能-在那之后不久,我无法实现Sudoku求解器,我仍然成为一名成功的软件工程师。
I do think that some computer science fundamentals can be very helpful, even for new developers. For example, the concepts behind Big-O notation can be really helpful for deciding between approaches. That being said, most data structures and algorithms aren’t used on a day to day basis, so why are they the basis for interviews and computer science classes instead of the more important things used every day?
我确实认为,即使对于新开发人员,某些计算机科学基础知识也可能会非常有帮助。 例如,Big-O表示法背后的概念对于确定方法之间确实很有帮助。 话虽这么说,大多数数据结构和算法不是每天使用的,那么为什么它们是面试和计算机科学课程的基础,而不是每天使用的更重要的东西?
I’m happy to see my own personal growth in coding; however, I can’t wait for a day when developers aren’t jumping through imaginary hoops to prove themselves, and when learning environments are much more constructive.
我很高兴看到自己在编码方面的个人成长; 但是,我迫不及待地希望开发人员没有经历过虚构的过程来证明自己,并且学习环境更具建设性。
If you liked this article, please subscribe to my weekly newsletter where you’ll receive my favorite links from the week and my latest articles.
如果您喜欢这篇文章,请订阅我的每周新闻,您将收到本周我最喜欢的链接以及我的最新文章。
翻译自: https://www.freecodecamp.org/news/coming-back-to-old-problems-how-i-finally-wrote-a-sudoku-solving-algorithm-3b371e6c63bd/
数独求解算法