【问题标题】:How does this Sudoku backtracking recursive algorithm trigger backtracking?这个数独回溯递归算法如何触发回溯?
【发布时间】:2021-04-28 17:08:07
【问题描述】:
let row = this.findEmpty(puzzleString)[0];
let col = this.findEmpty(puzzleString)[1];
let i = this.findEmpty(puzzleString)[2];

if(!this.findEmpty(puzzleString)) return puzzleString
for(let num = 1; num < 10; num++){
  if(this.checkValue(puzzleString, row, col, num)){
    puzzleString[i] = num;
    this.solve(puzzleString)
  } 
}

findEmpty(puzzleString) 遍历拼图字符串并返回空白网格的行 (A-I)、列 (1-9) 和索引。 checkValue() 包含 3 个帮助函数,返回一个布尔值,确保行、列或区域之间没有冲突。

循环从 1-9 开始迭代,将 1-9 中第一个通过 checkValue() 的值分配给当前空白网格,然后通过调用父函数 solve() 触发递归。

我不明白的是下一个语句以及它如何触发回溯

if(this.findEmpty(puzzleString)){ 
  puzzleString[i] = '.';
}

如果当前被检查的空白网格没有解决方案,那么我认为该网格仍然是空白 ('.')。如果这是正确的,为什么这个陈述是必要的?这个语句触发回溯怎么办?

我最初的倾向是该语句是一个伪-else 语句,仅当循环无法找到解决方案时才运行。它必须放在循环之外以允许从 1 到 9 的完整迭代。但是,如果只有在 checkValue() 成功时才调用 solve(),那么代码如何知道在之后运行 solve()

这是完整的代码:

solve(puzzleString) {
let row = this.findEmpty(puzzleString)[0];
let col = this.findEmpty(puzzleString)[1];
let i = this.findEmpty(puzzleString)[2];

if(!this.findEmpty(puzzleString)) return puzzleString
for(let num = 1; num < 10; num++){
  if(this.checkValue(puzzleString, row, col, num)){
    puzzleString[i] = num;
    this.solve(puzzleString)
  } 
}
if(this.findEmpty(puzzleString)){ 
  puzzleString[i] = '.';
} 

if(puzzleString.includes('.')) return { error: 'Puzzle cannot be solved' } 

return {
  solution: puzzleString.join('')
  }

}

findEmpty(puzzleString){
    for(let i = 0; i < puzzleString.length; i++){
      if(puzzleString[i] == '.'){
        let row = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
        let col = (i % 9) + 1;
        return [row, col, i];
      }
    } 
    return false;
  }

  checkValue(puzzleString, row, column, value){
    if(this.checkRowPlacement(puzzleString, row, column, value)&&
    this.checkColPlacement(puzzleString, row, column, value)&&
    this.checkRegionPlacement(puzzleString, row, column, value)){
      return true;
    }
    return false;
  }
checkRowPlacement(puzzleString, row, column, value) {

    let coordinates = [];
    let rowLetter;
    let colNum;
    let temp = [];
    if(row){row = row.toUpperCase();}
    for(let i = 0; i < puzzleString.length; i++){
      rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
      colNum = (i % 9) + 1;
      coordinates.push(rowLetter + colNum);
    } 
    for(let i = 0; i < coordinates.length; i++){
        if(coordinates[i][0] == row){
            temp.push(puzzleString[i]);
        }
    } 
    temp = temp.join('');
    return !temp.includes(value) ? true : false
    
  }

  checkColPlacement(puzzleString, row, column, value) {
    let coordinates = [];
    let rowLetter;
    let colNum;
    let temp = [];
    if(row){row = row.toUpperCase();}
    for(let i = 0; i < puzzleString.length; i++){
      rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
      colNum = (i % 9) + 1;
      coordinates.push(rowLetter + colNum);
    } 
    for(let i = 0; i < coordinates.length; i++){
        if(coordinates[i][1] == column){
            temp.push(puzzleString[i]);
        }
    } 
    temp = temp.join('');
    return !temp.includes(value) ? true : false
  }

  checkRegionPlacement(puzzleString, row, column, value) {
    let coordinates = [];
    let rowLetter;
    let colNum;
    let regions = [];
    if(row) row = row.toUpperCase();

    for(let i = 0; i < puzzleString.length; i++){
      rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
      colNum = (i % 9) + 1;
      coordinates.push(rowLetter + colNum);
    }

    for(let i = 0; i < coordinates.length; i+=27){
      for(let k = 0; k < 9; k+=3){
        regions.push(
          coordinates.slice(i+k,i+k+3) + ',' +
          coordinates.slice(i+k+9, i+k+12) + ',' +
          coordinates.slice(i+k+18, i+k+21)
        )
      }
    }

    let region = regions.filter(x => x.includes(row + column))[0].split(',').map(x => puzzleString[coordinates.indexOf(x)]).join('');

    return region.includes(value) ? false : true;
  }

【问题讨论】:

  • 注:这确实是一个糟糕的实现。它在相同的状态下使用相同的参数多次调用findEmpty
  • 你问了一个关于没有出现在第一个代码块中的一段代码的问题。你能澄清一下吗?
  • 请将其整合到一个代码块中,还包括function 标头(我想是solve 函数的代码),包括参数等。
  • 此代码不起作用。它没有成功解决数独。
  • 请提供函数findEmpty的代码。

标签: javascript algorithm recursion backtracking sudoku


【解决方案1】:

回溯发生在findEmtpy 返回false 时。但是由于您的代码不是最佳的,因此在回溯时仍然尝试了许多其他选项:递归树中未决的 for 循环都没有被中断,但是让它们继续并调用 checkValue 作为每个循环都是浪费精力这些电话现在将返回false。所以,最终所有那些for 循环都会结束,递归将回溯,只是为了完成另一个循环并再次回溯,......等等。

这里是您的主要功能的更新,以避免一些导致没有收益的开销:

solve(puzzleString) {
    // Only call findEmpty once!
    let emptyCell = this.findEmpty(puzzleString);
    if (!emptyCell) return { solution: puzzleString.join('') }; // return the success object
    let [row, col, i] = emptyCell; // use destructuring assignment
    for (let num = 1; num < 10; num++) {
        if (this.checkValue(puzzleString, row, col, num)) {
            puzzleString[i] = num;
            let result = this.solve(puzzleString); // capture the return value
            if (result.solution) return result; // success: fast backtracking!
        }
    }
    puzzleString[i] = "."; // could not solve this spot
    // backtrack to possibly take a different route in previous decisions
    return { error: 'Puzzle cannot be solved' };
}

【讨论】:

  • 如果checkValue() 无法为网格分配数字,该网格不是保持空白吗?如果还是空白,为什么还要加上puzzleString[i] = ".";
  • 因为可能有一个迭代,其中checkValue() 为真,但递归调用无法完成拼图剩余部分的工作,因此应该收回该移动。在递归调用后立即执行此操作不是绝对必要的,因为下一次迭代将跳过或覆盖它,但在循环完成后,应该清理它。
  • 但是如果你觉得它更直观,你可以把这个赋值移到 if 块中,作为其中的最后一条语句。这样,它与赋值 puzzleString[i] = num 发生在同一块中,并且更容易看到它正在撤消什么。
  • puzzleString[i] = "." 放在if 块中确实有效,但是如果checkValue() 永远不会返回true,为什么还要运行?
  • 确实,如果if 条件从不为真,则不必这样做,但我们需要额外检查以知道它从不为真。但是,由于在实际条件从未为真的情况下这样做并没有什么坏处,所以我们只是在不添加任何此类逻辑的情况下这样做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-25
  • 2021-12-28
  • 1970-01-01
  • 1970-01-01
  • 2011-12-27
  • 2017-12-16
相关资源
最近更新 更多