我之所以创建这个新主题,而不是仅仅阅读之前给出的这个特定问题的答案,是因为我觉得我并不完全理解它背后的整个想法。我似乎无法理解整个回溯概念。所以我需要完全理解回溯,然后解决特定的数独问题。
到目前为止我所理解的是,如果发现在当前状态之前作出的决定导致死胡同,则回溯是一种返回(例如)递归流的技术。所以你回去尝试别的东西然后再继续。
所以在我的数独例子中,我选择了第一个空单元格并尝试填写{1 ... 9}中的自然数,这与众所周知的数独规则不冲突。现在我对下一个空单元格做同样的事情,直到我到达没有冲突的情况下没有插入有效数字的点。根据我的理解,这应该是回溯起作用的地方。但是怎么样?如果我使用递归返回到最后写入的单元格,算法将再次填写数字,继续并最终陷入无限循环。
因此,我在互联网上搜索了一些提示,发现这是一个记录良好且经常解决的问题。然而,许多解决方案声称使用回溯我即使我的源代码就在我面前,也看不出它们是如何做到的。
示例包括: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;
}
答案 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
}
}
}
}
这当然只描绘了递归周围代码轮廓的低效示例。然而,我希望这显示了一种使用递归来调查所有可能性(给定一个板和一个状态)的方法,同时在给定状态不能解决的情况下启用回溯。