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