-1
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;
  }
  • 1
    NB: That is really a bad implementation. It calls the `findEmpty` several times with the same arguments in the same state. – trincot Jan 24 '21 at 20:27
  • 1
    You ask a question about a piece of code that does not appear in the first code block. Can you clarify? – trincot Jan 24 '21 at 20:28
  • The 2nd code appears right after the first code block – EzekielUmanmah Jan 24 '21 at 20:29
  • 1
    Please integrate it in one block of code, also including the `function` header (I suppose it is code of the `solve` function) including the arguments, ...etc. – trincot Jan 24 '21 at 20:31
  • Ok, I added the full `solve()` at the bottom. – EzekielUmanmah Jan 24 '21 at 20:32
  • 1
    This code doesn't work. It doesn't solve sudoku successfully. – trincot Jan 24 '21 at 20:34
  • It does work as I have a working version in my project. It might not work for you because I've not posted the extraneous helper functions. – EzekielUmanmah Jan 24 '21 at 20:35
  • Welcome to SO! Although this post and code is really non-ideal for reasons above, if you're talking about a typical Sudoku backtracking solver, yeah, you try every option for a square and if none of them led to a solution, you must have guessed wrongly at a previous square, so set the current square back to its original value and return control to the caller (backtrack) so it can try the next value in its loop over all possible values. It's basically brute force with pruning off hopelessly unsolvable paths. – ggorlen Jan 24 '21 at 20:41
  • 1
    Please provide the code for the function `findEmpty`. – trincot Jan 24 '21 at 20:42
  • I'm a beginner and am just happy that I got this far. I realize my code is not optimal but at this point I'm still learning the basics. I understand the idea behind backtracking. What I don't understand is if `checkValue()` fails then `solve()` is never called and I'm thinking recursion shouldn't occur. Additionally I think the blank grid being iterated **remains** blank and so my question is why is it necessary to reset it to blank and how does that trigger **backtracking**. – EzekielUmanmah Jan 24 '21 at 20:43
  • I've added the helper functions. – EzekielUmanmah Jan 24 '21 at 20:54

1 Answers1

0

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' };
}
trincot
  • 317,000
  • 35
  • 244
  • 286
  • If `checkValue()` cannot assign a number to a grid, doesn't that grid remain blank? If it remains blank, why is it necessary to add `puzzleString[i] = ".";`? – EzekielUmanmah Jan 25 '21 at 18:45
  • Because there might have been an iteration where `checkValue()` was true, but the recursive call could not finish the job for the remaining part of the puzzle, and so that move should be taken back. It is not absolutely necessary to do this immediately after the recursive call, since the next iteration will either skip or overwrite it, but after the loop has finished, this should be cleaned up. – trincot Jan 25 '21 at 18:49
  • But if you find it more intuitive, you could move this assignment inside the `if` block, as the last statement in there. That way it happens in the same block as the assignment `puzzleString[i] = num`, and it is easier to see what it is undoing. – trincot Jan 25 '21 at 18:52
  • Placing `puzzleString[i] = "."` in the `if` block does work but why should it run if `checkValue()` never returns true? – EzekielUmanmah Jan 25 '21 at 18:56
  • Indeed it doesn't have to if the `if` condition was never true, but we would need an extra check to *know* it was never true. But since it doesn't hurt to *also* do it when actually that condition was never true, we just do it without adding any such logic. – trincot Jan 25 '21 at 19:00
  • But if it is more intuitive you could write `if (puzzleString[i] != ".") puzzleString[i] = ".";`. My point is: whatever the value was, after this, it will always be ".", so why not just do it? – trincot Jan 25 '21 at 19:02
  • Logging `i` inside the loop but outside the `if` block shows the current `emptyCell` being checked. But if a solution cannot be found for the current empty cell, inside the `if` block `i` is already the previously filled empty cell. This clears up why inside the `if` block `puzzleString[i]` needs to be reset to `'.'` but how does the `if` block automatically receive the index of the last filled empty cell? Shouldn't `i` still be the index of the currently iterated cell? – EzekielUmanmah Jan 26 '21 at 18:57
  • In one call of `solve`, the value of `i` is constant. It is read before the loop and never changes. Every execution context of `solve` has its own, independent instance of an `i` variable. – trincot Jan 26 '21 at 18:58