Java中的数独求解器,使用回溯和递归

Posted

技术标签:

【中文标题】Java中的数独求解器,使用回溯和递归【英文标题】:Sudoku solver in Java, using backtracking and recursion 【发布时间】:2012-02-22 23:07:22 【问题描述】:

我正在用 Java 为 9x9 网格编写数独求解器。

我有以下方法:

打印网格

使用给定值初始化板

测试冲突(如果相同的数字在同一行或 3x3 子网格中)

一种逐一放置数字的方法,这需要最多的工作。

在我详细介绍该方法之前,请记住,我必须使用递归来解决它,以及回溯(以此处的小程序为例 http://www.heimetli.ch/ffh/simplifiedsudoku.html

另外,我通过垂直向下移动来解决这个数独问题,从左上角开始,通过第一列,然后通过第二列,等等。

到目前为止,我有以下内容:

public boolean placeNumber(int column)

    if (column == SUDOKU_SIZE)  // we have went through all the columns, game is over

        return true;

    

    else
    
        int row=0;  //takes you to the top of the row each time

        while (row < SUDOKU_SIZE)    loops through the column downwards, one by one
        

            if (puzzle[row][column]==0)  //skips any entries already in there (the given values)

                puzzle[row][column]=1;   //starts with one

                while(conflictsTest(row,column))   //conflictsTest is the method I wrote, which checks if the given parameters are in conflict with another number

                    puzzle[row][column] += 1;  

                


           //BACK TRACKING 

                placeNumber(column);      //recursive call

            
            else
              row++;                  // row already has a number given, so skip it
            
        

        column++;              // move on to second column
        placeNumber(column);

    
    return false; // no solutions to this puzzle

我标记为 BACKTRACKING 的地方是我认为我的其余代码需要去的地方。

我想到了一些类似的东西:

如果值为 10,则将该值设置回零,返回一行,然后将该值加 1

由于以下几个原因,这种回溯“策略”并不完全奏效:

    如果上一行是一个给定的值(也就是说,我不应该增加或触摸它,而是返回到我放置在那里的最后一个值)

    李>

    如果之前的值是 9。如果我将它增加 1,现在我们是 10,这是行不通的。

有人可以帮帮我吗?

【问题讨论】:

+1 表示想学习,而不仅仅是想被喂食代码。 出于同样的原因向我 +1。但你知道吗?我敢打赌,仍然会有代码。有些人就是无法抗拒。 【参考方案1】:

我不知道您将如何解决数独问题,但即使您使用蛮力方法(在我看来您所描述的内容),您也应该考虑到您的数据结构不合适。

我的意思是每个单元格不应该只是一个数字,而是一组数字(可能放置在那里)。

因此,给定的数字将表示为单例集,而您可以使用 1,2,3,4,5,6,7,8,9 初始化的空集。 然后目标是减少非单例单元格,直到所有单元格都是单例。

(请注意,在用铅笔和纸解决数独时,人们通常会在空白单元格中写下小数字,以跟踪那里可能出现的数字,只要你已经解决了。)

然后,当“尝试下一个数字”时,您会从集合中取出下一个数字。给定的单元格没有下一个数字,因此您无法更改它们。这样,您描述的困难就消失了(至少有点)。

----- 编辑,在了解到需要蛮力之后。

你的老师显然想教你递归的奇迹。很好!

在这种情况下,我们只需要知道哪些单元格是给定的,哪些不是。

可以在这里使用的一种特别简单的方法是在任何未给定的单元格中放置一个 0,因为根据定义,给定的单元格是 1、2、3、4、5、6、7、8、9 之一。

现在让我们考虑如何使递归蛮力发挥作用。

我们的目标是解决具有 n 个空单元格的数独。 如果我们有一个函数可以解决具有 n-1 个空单元格的数独(或表示它无法解决),那么这个任务将很容易:

let c be some empty cell.
let f be the function that solves a sudoku with one empty cell less.
for i in 1..9
   check if i can be placed in c without conflict
   if not continue with next i
   place i in c
   if f() == SOLVED then return SOLVED
return NOTSOLVABLE

此伪代码选择一些空单元格,然后尝试所有适合该单元格的数字。 因为数独 - 根据定义 - 只有一个解决方案,所以只有以下情况:

我们选择了正确的号码。然后 f() 将找到解决方案的其余部分并返回 SOLVED。 我们选择了一个错误的数字:f() 将发出信号,表明我们单元格中的错误数字无法解决数独问题。 我们检查了所有数字,但没有一个是正确的:然后我们自己得到了一个无法解决的数独,我们向调用者发出信号。

不用说,该算法基于这样一个假设,即我们只放置不存在的数字 与当前状态相冲突。例如,当在同一行、列或框中已经有 9 时,我们不会在其中放置 9

如果我们现在想一想我们神秘但未知的函数f() 的样子,事实证明它与我们已经拥有的几乎相同! 我们尚未考虑的唯一情况是具有 0 个空单元格的数独。这意味着,如果我们发现没有更多的空单元格,我们就知道我们刚刚解决了数独问题并返回了刚刚解决的问题。

这是编写旨在解决问题的递归函数时的常见技巧。我们正在编写solve(),而且我们知道,这个问题是完全可以解决的。因此,我们已经可以使用我们刚刚编写的函数,只要我们确保在每次递归时,问题都会以某种方式更接近解决方案。最后,我们达到了所谓的基本情况,在这种情况下,我们可以在不进一步递归的情况下给出解决方案。

在我们的例子中,我们知道数独是可解的,此外,我们知道它只有一个解。通过将一块放在一个空单元格中,我们更接近于解决方案(或接近没有解决方案的诊断),并将新的、更小的问题递归地提供给我们正在编写的函数。基本情况是“具有 0 个空单元格的数独”,实际上 是解决方案

(如果有很多可能的解决方案,事情会变得有点复杂,但我们将其留到下一课。)

【讨论】:

是的,我正在使用蛮力方法,这是分配要求所要求的。我之所以选择这种数据结构,是因为我的教科书中有一个类似的例子。我们被告知我们可以实现自己的解决方案,或者使用教科书中的解决方案作为起点。我指的教科书中的例子是“8皇后”问题的回溯递归解决方案,这与实现数独略有不同 好吧,在这种情况下,你不需要一个集合,但也许只是一个告诉你是否给定单元格的标志。 您对如何实现这样的标志有任何建议或提示吗? 我会将求解器的返回类型替换为一个数字,该数字给出找到的解决方案的数量。在普通情况下,返回 1。在所有其他情况下,返回递归结果的总和。 (不要用空板来尝试,除非你要休假几周。)如果我们必须保留所有解决方案(而不仅仅是它们的数量),事情会变得更加复杂。 打印不是问题(即在 Java 中)。只有一个地方(微不足道的案例),你有一个解决方案。只需在返回 1 之前打印它。如果我们必须保留它,我们将不得不维护一个列表或一些容器,并复制周围的东西。但是有了这个要求,它就没有那么复杂了。打印它,然后忘记它。 (如果我是你的老师,我会先打印解决方案的数量,然后再打印解决方案本身。)【参考方案2】:

首先,优化建议:在检查您要放入单元格中的数字是否已经存在于同一行、列或小网格中时,您没有运行一个循环或类似的东西。您可以通过数组索引执行即时检查。

考虑 3 个 9x9 布尔双维数组:

boolean row[9][9], col[9][9], minigrid[9][9]

我们将使用第一个数组来检查数字是否存在于同一行中,第二个数组用于检查数字是否存在于同一列中,第三个数组用于迷你网格。

假设您想在单元格 ij 中输入一个数字 n。您将检查 row[i][n-1] 是否为真。如果是,则 ith 行已经包含 n。同样,您将检查 col[j][n-1]minigrid[gridnum][n-1] 是否为真。

这里gridnum是小网格的索引,你要插入数字的单元格所在的位置。计算单元格i,j的小网格数, i & j 除以 3,将前者的整数部分乘以 3,再与后者的整数部分相加。

看起来是这样的:

gridnum = (i/3)*3 + j/3

通过查看所有 i 和 j 的 i/3 和 j/3 值,您将了解其工作原理。此外,如果您在单元格中输入数字,也要更新数组。例如。 行[i][n-1] = true

如果有你不明白的部分,发表评论,我会编辑我的答案来解释它。

其次,使用递归和回溯来解决这个问题很容易。

boolean F( i, j) // invoke this function with i = j = 0

If i > 8: return true // solved

for n in 1..9
 
 check if n exists in row, column, or mini grid by the method I described

 if it does: pass ( skip this iteration )

 if it doesn't
  
   grid[i][j] = n
   update row[][], col[][], minigrid[][]

   if F( if j is 8 then i+1 else i, if j is 8 then 0 else j+1 ) return true // solved
  
 
 return false // no number could be entered in cell i,j

【讨论】:

【参考方案3】:

我建议将当前行和列都传递给递归方法 然后找到该单元格的所有允许的数字,对于每个允许的数字,递归调用下一列(或下一行,如果在最后一列)的方法,如果移动导致死轨,则撤消移动

public boolean fillCell(int r, int c) 
    if last row and last cell 
        //report success
        return true
    
    for i 1 to 9 
        if can place i on r, c 
            board[r][c] = i
            if !fillCell(next empty row, next empty column)  //DONT change r or c here or you will not be able to undo the move
                board[r][c] = 0
            
            /*
            else 
                return true; //returning true here will make it stop after 1 solution is found, doing nothing will keep looking for other solutions also
            
            */

        
    
    return false        

【讨论】:

【参考方案4】:

如果找不到解决方案,我会检查每个单元格并返回递归步骤。

更详细: 转到下一个单元格,如果值 x == 0,检查 x+1 是否有效,如果为真,则通过使用下一个可能的单元格递归调用方法来转到下一个单元格。如果数字无效,检查 x+2 等。如果没有数字有效,则返回 false 并重复上一次调用中的 x+1 步骤。如果你点击了一个里面有数字的单元格,不要调用递归而是直接进入下一个,因此你不需要标记任何预先输入的单元格。

伪代码:

fillcell cell
 while cell is not 0
  cell = next cell
 while cell value < 10
  increase cell value by one
  if cell is valid 
    if fillcell next cell is true
      return true
return false

不确定这是否正确,但它应该表明这个想法。

【讨论】:

【参考方案5】:

一些可能有用的想法(关于递归和回溯)

//some attributes you might need for storing e.g. the configuration to track back to.

boolean legal(Configuration configuration) 



int partSolution(Configuration configuration) 
  if (legal(configuration))
    return partSolution(nextConfiguration())
  else
    return partSolution(previousConfiguration())    


Configuration nextConfiguration() 
 //based on the current configuration and the previous tried ones,
 //return the next possible configuration:
 //next number to enter, next cell to visit


Configuration previousConfiguration() 
 //backtrack


solve () 
  call partSolution with start configuration while partSolution < 9x9

编写一个配置类,其中包含输入的数字和一些其他属性(如输入的大小和#numbers)并考虑还需要什么

【讨论】:

【参考方案6】:

此页面上的其他答案已涵盖回溯算法。有趣的是,只需稍加优化,您就可以显着改进这个回溯算法。这个想法是使用贪婪的最佳优先搜索:你不是从上到下、从左到右选择“下一个”单元格,而是选择下一个单元格作为数量最少的单元格可能性

例如,如果包含该单元格的行已经有数字 1 2 3,列有 4 5 6,而 3x3 块有 7,那么只剩下 2 种可能性:8 和 9。这看起来像很不错的细胞选择。

这项改进大大加快了程序的运行速度,使程序运行得足够快my Real-time Sudoku Solver

你可以查看这个算法的动画here。

链接到Visualizer Code 和Real-time Solver Code

贪婪的最佳优先搜索的代码如下:

# Keep data about the "Best" cell
class EntryData:
    def __init__(self, r, c, n):
        self.row = r
        self.col = c
        self.choices = n

    def set_data(self, r, c, n):
        self.row = r
        self.col = c
        self.choices = n

# Solve Sudoku using Best-first search
def solve_sudoku(matrix):
    cont = [True]
    # See if it is even possible to have a solution
    for i in range(9):
        for j in range(9):
            if not can_be_correct(matrix, i, j): # If it is not possible, stop
                return
    sudoku_helper(matrix, cont) # Otherwise try to solve the Sudoku puzzle

# Helper function - The heart of Best First Search
def sudoku_helper(matrix, cont):
    if not cont[0]: # Stopping point 1
        return

    # Find the best entry (The one with the least possibilities)
    best_candidate = EntryData(-1, -1, 100)
    for i in range(9):
        for j in range(9):
            if matrix[i][j] == 0: # If it is unfilled
                num_choices = count_choices(matrix, i, j)
                if best_candidate.choices > num_choices:
                    best_candidate.set_data(i, j, num_choices)

    # If didn't find any choices, it means...
    if best_candidate.choices == 100: # Has filled all board, Best-First Search done! Note, whether we have a solution or not depends on whether all Board is non-zero
        cont[0] = False # Set the flag so that the rest of the recursive calls can stop at "stopping points"
        return

    row = best_candidate.row
    col = best_candidate.col

    # If found the best candidate, try to fill 1-9
    for j in range(1, 10):
        if not cont[0]: # Stopping point 2
            return

        matrix[row][col] = j

        if can_be_correct(matrix, row, col):
            sudoku_helper(matrix, cont)

    if not cont[0]: # Stopping point 3
        return
    matrix[row][col] = 0 # Backtrack, mark the current cell empty again
            

# Count the number of choices haven't been used
def count_choices(matrix, i, j):
    can_pick = [True,True,True,True,True,True,True,True,True,True]; # From 0 to 9 - drop 0
    
    # Check row
    for k in range(9):
        can_pick[matrix[i][k]] = False

    # Check col
    for k in range(9):
        can_pick[matrix[k][j]] = False;

    # Check 3x3 square
    r = i // 3
    c = j // 3
    for row in range(r*3, r*3+3):
        for col in range(c*3, c*3+3):
            can_pick[matrix[row][col]] = False

    # Count
    count = 0
    for k in range(1, 10):  # 1 to 9
        if can_pick[k]:
            count += 1

    return count

# Return true if the current cell doesn't create any violation
def can_be_correct(matrix, row, col):
    
    # Check row
    for c in range(9):
        if matrix[row][col] != 0 and col != c and matrix[row][col] == matrix[row][c]:
            return False

    # Check column
    for r in range(9):
        if matrix[row][col] != 0 and row != r and matrix[row][col] == matrix[r][col]:
            return False

    # Check 3x3 square
    r = row // 3
    c = col // 3
    for i in range(r*3, r*3+3):
        for j in range(c*3, c*3+3):
            if row != i and col != j and matrix[i][j] != 0 and matrix[i][j] == matrix[row][col]:
                return False
    
    return True

# Return true if the whole board has been occupied by some non-zero number
# If this happens, the current board is the solution to the original Sudoku
def all_board_non_zero(matrix):
    for i in range(9):
        for j in range(9):
            if matrix[i][j] == 0:
                return False
    return True

【讨论】:

以上是关于Java中的数独求解器,使用回溯和递归的主要内容,如果未能解决你的问题,请参考以下文章

Gui 可视化递归回溯数独

无法回溯以使用递归 javascript 数独求解器

Javascript递归回溯数独求解器

具有回溯的数独求解器不能总是检测到多个解决方案

如何通过回溯和递归解决数独?

为啥这个“数独求解器”算法不起作用