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

Posted

技术标签:

【中文标题】如何通过回溯和递归解决数独?【英文标题】:How to solve Sudoku by backtracking and recursion? 【发布时间】:2016-02-07 14:58:11 【问题描述】:

我之所以创建这个新线程,而不是仅仅阅读之前给出的这个特定问题的答案,是因为我觉得我只是不完全理解它背后的整个想法。我似乎无法理解整个回溯的概念。所以我需要充分理解回溯,然后解决特定的数独问题。

到目前为止,我所理解的是,如果发现在当前状态之前做出的决定导致了死胡同,则回溯是一种返回的技术,在(例如)递归流程中。所以你回去试试别的,然后再继续。

因此,在我的数独示例中,我选择了第一个空单元格并尝试从 1...9 中填充一个自然数,该自然数与众所周知的数独规则不冲突。现在我对下一个空单元格做同样的事情,直到我没有插入有效数字而不会发生冲突。据我了解,这应该是回溯发挥作用的地方。但是怎么做?如果我使用递归返回最后写入的单元格,算法将再次填写数字,继续并最终陷入无限循环。

所以我在 Internet 上搜索了提示,发现这是一个有据可查且经常解决的问题。然而,许多解决方案声称使用回溯,即使我面前有源代码,我也不知道它们是如何或在何处使用的。

例如:Sudoku solver in Java, using backtracking and recursion 或 http://www.heimetli.ch/ffh/simplifiedsudoku.html

这是我的(不工作的)源代码:

private boolean isSolvable( Sudoku sudoku, int row, int col )

    //if the method is called for a row > 8 the sudoku is solved
    if(row > 8)
        return true;

    //calculate the next cell, jump one row if at last column
    int[] nextCell = (col < 8) ? new int[]row,col+1 : new int[]row+1,0;

    //check if the current cell isWritable() that is if it is not a given cell by the puzzle
    //continue recursively with the next cell if not writable
    if(!sudoku.isWritable(row, col))
        isSolvable(sudoku, nextCell[0], nextCell[1]);
    else
        //set the current cell to the lowest possible not conflicting number
        for(int i=1; i< 10; i++)
            if(!conflictAt(sudoku, row, col, i))
                sudoku.setValue(row, col, i);

                //continue recursively with the next cell
                isSolvable(sudoku, nextCell[0], nextCell[1]);
            
        
    

    return false;

现在我不知道如何继续。如何实施回溯或者我已经实施了吗?这似乎是一个愚蠢的问题,但我真的没有看到上面链接中提到的源代码中有更多的回溯。

编辑:最终(工作)版本:

private boolean isSolvable( Sudoku sudoku, int row, int col )

    //if the method is called for a row > 8 the Sudoku is solved
    if(row > 8)
        return true;

    //if the cell is not writable, get the next writable cell recursively
    if(!sudoku.isWritable(row,col))
        return isSolvable(sudoku, (col<8) ? row : row + 1, (col<8) ? col + 1 : 0);

    //brute forcing for solution
    for(int i=1; i<=9; i++)
        if(!conflictAt(sudoku, row, col, i))
            sudoku.setValue(row, col, i);
            if(isSolvable(sudoku, (col<8) ? row : row + 1, (col<8) ? col + 1 : 0)) return true; 
        
    
    sudoku.setValue(row, col, 0);
    return false;

【问题讨论】:

您可以使用各种策略直接解决问题,而不是猜测解决方案:github.com/cyrixmorten/sudoku。如果遇到死胡同(如回溯),这也留下了交换策略的选项。您可以查看项目中的单元测试以查看添加新策略以解决困难数独的示例。此实现的另一个好处是能够让用户逐步推理如何找到解决方案。 嗨,我知道有更好、更有效的方法来解决这个问题。我是一名 Java 新手和计算机科学专业的学生(第一学期),因此需要了解这个确切的实现。 好吧,我明白了,那么当你需要考虑基本的软件架构时,你可能想看看我的项目 :) 似乎是第一学期的一个很好且相当高级的作业。 我可能会的,谢谢。 :) 【参考方案1】:

我只是解释一下回溯的含义。

递归意味着从同一个函数中调用函数。现在发生的情况是,当函数遇到对自身的调用时..想象一个新页面打开并且控制从旧页面转移到这个新页面到函数的开始,当函数再次遇到调用时新页面,另一个页面在它旁边打开,这样新页面就会在旧页面旁边不断弹出。

返回的唯一方法是使用return 语句。当函数遇到它时,控件会从新页面返回到调用它的同一行上的旧页面,并开始执行该行下方的任何内容。这是回溯开始的地方。为了避免在数据填满时再次输入数据等问题,您需要在每次调用函数后添加一个 return 语句。

例如在您的代码中

if(row > 8)
        return true;

这是基本情况。当它为真时,该函数开始回溯,即控制从新页面返回到旧页面,但它从调用它的任何地方返回。例如,如果它是从此语句中调用的。

 for(int i=1; i< 10; i++)
            if(!conflictAt(sudoku, row, col, i))
                sudoku.setValue(row, col, i);

                //continue recursively with the next cell
                isSolvable(sudoku, nextCell[0], nextCell[1]);  <------ here
            

它将回到这一行并开始做它应该做的任何事情。该语句在 for 循环内,如果 i &lt; 10 循环将运行,它将再次尝试设置值。这不是您想要的,您希望它继续回溯直到它退出功能,因为数独已满,对吗?为此,您需要在此调用之后添加 return 语句,即 return true; 我还没有阅读您的代码,因此可能会有更多类似的错误。

【讨论】:

很好的答案,谢谢!我想我现在掌握了这个概念,所以我回到 Eclipse 并改进/更改了我的代码。我将它插入到我的原始帖子中。你能看一下吗?提前致谢。 我看到的一个错误是 for 循环中的 if 部分。您的评论说“跳过此迭代”,但我没有看到任何继续关键字可以跳过它。除此之外,它会引发任何错误/异常吗? 我认为这里不需要“继续”,因为 for 循环中有一个 if/else 语句,其中 if 子句为空。因此,每当执行 if 部分时,什么都不会发生,并且迭代会继续。实际上没有错误或异常被抛出。它只是返回真,数独有些解决但其中有一些零。 哦,是的,我的错。我忘了它在if else中。所以它的工作原理? 它不能正常工作,但现在我明白了。更新后的初始帖子现在包含我的解决方案。再次感谢您的意见,您的回答确实对一般理解有很大帮助。【参考方案2】:

实现回溯的最简单方法是使用堆栈。让您的数独板成为一类,包括所有确定的数字和可能的数字。每当您需要选择一个数字时,您都会创建一个板的副本。一个副本将您在该方格中选择的数字标记为不可选择(您不想选择两次),然后将该副本放入堆栈。第二个副本您选择号码并照常进行。

每当你走到死胡同时,你就扔掉你正在处理的板子,从堆栈中取出最上面的板子,然后继续那个板子。这是“回溯”部分:您回到以前的状态并再次尝试不同的路径。如果您之前选择了 1 但它不起作用,那么您从同一位置重试,但选择 2。

如果数独是可解的,那么您最终会来到一个可以填写所有数字的板上。此时,您可以丢弃堆栈中剩余的任何零件板,因为您不需要它们。

如果你只是想生成可解的数独,那么你可以作弊,查看答案:How to generate Sudoku boards with unique solutions

【讨论】:

这是一个很好的方法,它帮助我理解。感谢那!但是,我在如何设计练习中给出的“数独数据结构”方面受到了一定的限制。它只有两个属性:“值”和“可写”,它们都是二维数组。 “值”保存单元格中的数字,“可写”保存每个单元格的布尔值,无论是否允许在单元格中写入。 要么编写一个包装类来保存给定的数据结构,要么运行两个并行堆栈,一个来保存每个数据类,并在访问堆栈时弹出/推送一个。跨度> 【参考方案3】:

我认为递归和回溯的方式如下:

调用 isSolvable() 应该尝试将传递的数独作为第一个参数,并从特定的行和列解决它(从而假设所有先前的值都是确定且有效的)。

计算出数独的完整解决方案将类似于以下代码。如果我正确理解rossum,这有点概括了相同的想法:

// you are handed a sudoku that needs solving
Sudoku sudoku; 
for (int row=0; row <= 9; row++) 
    for (int col=0; col <= 9; col++) 
        for (int value_candidate = 1; value_candidate <= 10; value_candidate++) 

            // or any other type of deep-copy 
            Sudoku sudokuCopy = sudoku.clone(); 
            sudokuCopy.setValue(row, col, value_candidate);

            if (isSolvable(sudokuCopy, row, col))  // (recursion)

                // only if the value_candidate has proven to allow the                  
                // puzzle to be solved, 
                // we persist the value to the original board

                sudoku.setValue(row, col, value_candidate);

                // and stop attempting more value_candidates for the current row and col 
                // by breaking loose of this for-loop
                continue;
             else  // (backtracking)

                // if the value_candidate turns out to bring no valid solution
                // move on to the next candidate while staying put at 
                // the current row and col 
            
        
    
  

这当然只是勾勒出递归代码大纲的低效示例。然而,我希望这显示了一种使用递归来调查所有可能性(给定棋盘和状态)的方法,同时在给定状态不具备解决方案的情况下启用回溯。

【讨论】:

以上是关于如何通过回溯和递归解决数独?的主要内容,如果未能解决你的问题,请参考以下文章

Python数独回溯

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

Gui 可视化递归回溯数独

优化回溯算法求解数独

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

Javascript递归回溯数独求解器