Search code examples
javascriptalgorithmrecursionbacktrackingsudoku

How does this Sudoku backtracking recursive algorithm trigger backtracking?


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) iterates over the puzzle string and returns the row (A-I), column (1-9), and index of a blank grid. checkValue() contains 3 helper functions returning a boolean ensuring there are no conflicts across row, column, or region.

The loop iterates from 1-9 and the first value from 1-9 that passes checkValue() is assigned to the current blank grid and then triggers recursion by calling the parent function solve().

What I don't understand is the next statement and how that triggers backtracking.

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

If the current blank grid being checked has no solution then I think the grid remains a blank ('.'). If this is correct, why is this statement necessary? What about this statement is triggering backtracking?

My initial inclination is that this statement is a psuedo-else statement that runs only if the loop fails to find a solution. It has to be placed outside the loop in order to allow the full iteration of 1 through 9. But then how does the code know to run solve() afterwards if solve() is only called if checkValue() suceeds?

Here's the full code:

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;
  }

Solution

  • The backtracking happens when findEmtpy returns false. But as your code is not optimal, still many other options are tried while backtracking: none of the for loops that are pending in the recursion tree are interrupted, yet it is wasted effort to have them continue and calling checkValue as each of those calls will now return false. So, eventually all those for loops will end, and the recursion will backtrack, only to finish yet another loop and backtrack again, ...etc.

    Here is an update of your main function to avoid some of that overhead that leads to no gain:

    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' };
    }