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

时间:2016-02-07 14:58:11

标签: java recursion sudoku backtracking recursive-backtracking

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

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

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

因此,我在互联网上搜索了一些提示,发现这是一个记录良好且经常解决的问题。然而,许多解决方案声称使用回溯我即使我的源代码就在我面前,也看不出它们是如何做到的。

示例包括:Sudoku solver in Java, using backtracking and recursionhttp://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;
}

3 个答案:

答案 0 :(得分:1)

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

每当你走到死胡同时,你扔掉你正在工作的电路板,将顶板从堆叠中取出并继续使用该电路板。这是&#34;回溯&#34; part:你回到以前的状态,然后再试一次不同的路径。如果您之前选择了1并且它没有工作,那么您再次尝试从相同的位置,而是选择2。

如果Sudoku可以解决,那么你最终会进入一个可以填写所有数字的板子。此时,您可以丢弃任何留在堆叠上的部分板,因为您不需要它们。

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

答案 1 :(得分: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 < 10循环将运行,它将尝试再次设置值。这不是你想要的,你希望它继续回溯直到它退出函数,因为数独填满了吗?为了做到这一点,您需要在此次通话后发出return声明,return true;我还没有读取您的代码,因此可能会出现更多错误。

答案 2 :(得分:0)

我想到递归和回溯的方式如下:

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

制定数独的完整解决方案将类似于以下代码。如果我理解正确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 
            }
        }
    }
}  

这当然只描绘了递归周围代码轮廓的低效示例。然而,我希望这显示了一种使用递归来调查所有可能性(给定一个板和一个状态)的方法,同时在给定状态不能解决的情况下启用回溯。