From 45aaadc0560559845190247e06e9c8687c749555 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sat, 20 Jan 2024 20:37:02 -0500 Subject: [PATCH 01/11] Set up environment and begin implementation --- sudoku.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sudoku.go b/sudoku.go index 06ab7d0..eb17ae7 100644 --- a/sudoku.go +++ b/sudoku.go @@ -1 +1,32 @@ package main + +import "fmt" + +// Requires: The input grid will be a 9x9 two-dimensional array of integers. +// The input grid will have exactly one solution. +// Modifies: N/A +// Effects: Solves the sudoku, returns a 9 by 9 array of the solved sudoku +func SolveSudoku(sudoku [][]int) [][]int { + for i := 0; i < len(sudoku); i++ { + for j := 0; j < len(sudoku[i]); j++ { + fmt.Printf("%d ", sudoku[i][j]) + } + fmt.Println() + } + return sudoku +} + +func main() { + input := [][]int{ + {5, 3, 0, 0, 7, 0, 0, 0, 0}, + {6, 0, 0, 1, 9, 5, 0, 0, 0}, + {0, 9, 8, 0, 0, 0, 0, 6, 0}, + {8, 0, 0, 0, 6, 0, 0, 0, 3}, + {4, 0, 0, 8, 0, 3, 0, 0, 1}, + {7, 0, 0, 0, 2, 0, 0, 0, 6}, + {0, 6, 0, 0, 0, 0, 2, 8, 0}, + {0, 0, 0, 4, 1, 9, 0, 0, 5}, + {0, 0, 0, 0, 8, 0, 0, 7, 9}, + } + SolveSudoku(input) +} From aa036f3f239f617c29b45982cb2d53fbe3430f52 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 14:08:48 -0500 Subject: [PATCH 02/11] Design code structure using backtracking algorithm --- sudoku.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sudoku.go b/sudoku.go index eb17ae7..440bcdc 100644 --- a/sudoku.go +++ b/sudoku.go @@ -2,6 +2,23 @@ package main import "fmt" +// REQUIRES: board is a 2d array representing a sudoku with exactly 1 solution +// 0 <= row, col <= 8 +// MODIFIES: board +// EFFECTS: Performs a (recursive) backtracking algorithm to solve the sudoku +func SolveSudokuHelper(board [][]int, row int, col int) bool { + // If sudoku is solved, return true + // Otherwise + // Try to place number 1 through 9 at current row and col + // Check constraint: If placing this number does not violate any rules of sudoku + // Recursion with the next unfilled cell on the board + // If recursive function call returned true, then it means puzzle solved so return true + // Otherwise, Backtrack: "unplace" the number at current cell + // Reaching this point means that none of the numbers from 1 to 9 can be placed at current cell + // i.e. the number in a previous cell needs to change, thus Backtrack -> return to the caller! + return false +} + // Requires: The input grid will be a 9x9 two-dimensional array of integers. // The input grid will have exactly one solution. // Modifies: N/A @@ -13,6 +30,9 @@ func SolveSudoku(sudoku [][]int) [][]int { } fmt.Println() } + // calls helper function which does the backtracking + // pass sudoku 2d array by reference + SolveSudokuHelper(sudoku, 0, 0) return sudoku } From dea9005b758ede67ebdeb06877598fa53ddde8f0 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 14:45:19 -0500 Subject: [PATCH 03/11] Implement constraint checking --- sudoku.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/sudoku.go b/sudoku.go index 440bcdc..cbbf665 100644 --- a/sudoku.go +++ b/sudoku.go @@ -2,6 +2,32 @@ package main import "fmt" +// REQUIRES: existing board is valid +// 0 <= row, col <= 8; board[row][col] is unfilled +// 1 <= num <= 9 +// MODIFIES: N/A +// EFFECTS: Determines if placing num at board[row][col] is valid +func Promising(board [][]int, row int, col int, num int) bool { + // check all cells on the same row and same column as the cell at [row][col] + for i := 0; i < 9; i++ { + if board[row][i] == num || board[i][col] == num { + return false + } + } + // check all cells in the same 3 by 3 grid as the cell at [row][col] + startRowIdx := (row / 3) * 3 + startColIdx := (col / 3) * 3 + fmt.Printf("Start row idx: %d, Start col idx: %d\n", startRowIdx, startColIdx) + for i := startRowIdx; i < startRowIdx + 3; i++ { + for j := startColIdx; j < startColIdx + 3; j++ { + if board[i][j] == num { + return false + } + } + } + return true +} + // REQUIRES: board is a 2d array representing a sudoku with exactly 1 solution // 0 <= row, col <= 8 // MODIFIES: board @@ -10,10 +36,17 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { // If sudoku is solved, return true // Otherwise // Try to place number 1 through 9 at current row and col + for num := 1; num <= 9; num++ { // Check constraint: If placing this number does not violate any rules of sudoku + if Promising(board, row, col, num) { + // Place number + board[row][col] = num // Recursion with the next unfilled cell on the board + // If recursive function call returned true, then it means puzzle solved so return true // Otherwise, Backtrack: "unplace" the number at current cell + } + } // Reaching this point means that none of the numbers from 1 to 9 can be placed at current cell // i.e. the number in a previous cell needs to change, thus Backtrack -> return to the caller! return false @@ -36,7 +69,7 @@ func SolveSudoku(sudoku [][]int) [][]int { return sudoku } -func main() { +func testPromising() { input := [][]int{ {5, 3, 0, 0, 7, 0, 0, 0, 0}, {6, 0, 0, 1, 9, 5, 0, 0, 0}, @@ -48,5 +81,39 @@ func main() { {0, 0, 0, 4, 1, 9, 0, 0, 5}, {0, 0, 0, 0, 8, 0, 0, 7, 9}, } - SolveSudoku(input) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 5)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 7)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 4)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 9)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 3)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 8)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 6)) + fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 2, 0, 1)) + fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 2, 0, 2)) + + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 7)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 9)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 8)) + + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 8, 5, 4)) + fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 8, 5, 1)) + fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 8, 5, 6)) + fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 8, 5, 2)) +} + +func main() { + // input := [][]int{ + // {5, 3, 0, 0, 7, 0, 0, 0, 0}, + // {6, 0, 0, 1, 9, 5, 0, 0, 0}, + // {0, 9, 8, 0, 0, 0, 0, 6, 0}, + // {8, 0, 0, 0, 6, 0, 0, 0, 3}, + // {4, 0, 0, 8, 0, 3, 0, 0, 1}, + // {7, 0, 0, 0, 2, 0, 0, 0, 6}, + // {0, 6, 0, 0, 0, 0, 2, 8, 0}, + // {0, 0, 0, 4, 1, 9, 0, 0, 5}, + // {0, 0, 0, 0, 8, 0, 0, 7, 9}, + // } + // SolveSudoku(input) + + testPromising() } From 74a4dd336f47708b304ce30488b5f744eec24d17 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 15:10:40 -0500 Subject: [PATCH 04/11] Implement find next unfilled cell function --- sudoku.go | 76 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/sudoku.go b/sudoku.go index cbbf665..b94e003 100644 --- a/sudoku.go +++ b/sudoku.go @@ -17,7 +17,7 @@ func Promising(board [][]int, row int, col int, num int) bool { // check all cells in the same 3 by 3 grid as the cell at [row][col] startRowIdx := (row / 3) * 3 startColIdx := (col / 3) * 3 - fmt.Printf("Start row idx: %d, Start col idx: %d\n", startRowIdx, startColIdx) + // fmt.Printf("Start row idx: %d, Start col idx: %d\n", startRowIdx, startColIdx) for i := startRowIdx; i < startRowIdx + 3; i++ { for j := startColIdx; j < startColIdx + 3; j++ { if board[i][j] == num { @@ -28,6 +28,31 @@ func Promising(board [][]int, row int, col int, num int) bool { return true } +// REQUIRES: Existing board is valid +// 0 <= curRow, curCol <= 8 +// curRow, curCol corresponds to the cell that was just filled +// MODIFIES: N/A +// EFFECTS: Returns the row and column index of the next unfilled cell +// Returns -1, -1 if all cells are filled +func FindNextUnfilled(board [][]int, curRow int, curCol int) (int, int) { + // Look for unfilled cells in current row first + for j := curCol + 1; j < 9; j++ { + if board[curRow][j] == 0 { + return curRow, j + } + } + // Then move on to the next row(s) + for i := curRow + 1; i < 9; i++ { + for j := 0; j < 9; j++ { + if board[i][j] == 0 { + return i, j + } + } + } + // No unfilled cell found + return -1, -1 +} + // REQUIRES: board is a 2d array representing a sudoku with exactly 1 solution // 0 <= row, col <= 8 // MODIFIES: board @@ -42,7 +67,12 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { // Place number board[row][col] = num // Recursion with the next unfilled cell on the board - + nextRow, nextCol := FindNextUnfilled(board, row, col) + fmt.Printf("Next row: %d, Next col: %d\n", nextRow, nextCol) + // All cells filled -> Solved! + if nextRow < 0 { + return true + } // If recursive function call returned true, then it means puzzle solved so return true // Otherwise, Backtrack: "unplace" the number at current cell } @@ -57,15 +87,25 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { // Modifies: N/A // Effects: Solves the sudoku, returns a 9 by 9 array of the solved sudoku func SolveSudoku(sudoku [][]int) [][]int { + // for storing the index of the first unfilled cell + row := -1 + col := -1 for i := 0; i < len(sudoku); i++ { for j := 0; j < len(sudoku[i]); j++ { - fmt.Printf("%d ", sudoku[i][j]) + // Preprocess the board by filling all unfilled values with 0 + if sudoku[i][j] < 1 || sudoku[i][j] > 9 { + sudoku[i][j] = 0 + // Find the index of the first unfilled cell + if row == -1 { + row = i + col = j + } + } } - fmt.Println() } // calls helper function which does the backtracking // pass sudoku 2d array by reference - SolveSudokuHelper(sudoku, 0, 0) + SolveSudokuHelper(sudoku, row, col) return sudoku } @@ -102,18 +142,18 @@ func testPromising() { } func main() { - // input := [][]int{ - // {5, 3, 0, 0, 7, 0, 0, 0, 0}, - // {6, 0, 0, 1, 9, 5, 0, 0, 0}, - // {0, 9, 8, 0, 0, 0, 0, 6, 0}, - // {8, 0, 0, 0, 6, 0, 0, 0, 3}, - // {4, 0, 0, 8, 0, 3, 0, 0, 1}, - // {7, 0, 0, 0, 2, 0, 0, 0, 6}, - // {0, 6, 0, 0, 0, 0, 2, 8, 0}, - // {0, 0, 0, 4, 1, 9, 0, 0, 5}, - // {0, 0, 0, 0, 8, 0, 0, 7, 9}, - // } - // SolveSudoku(input) + input := [][]int{ + {5, 3, 0, 0, 7, 0, 0, 0, 0}, + {6, 0, 0, 1, 9, 5, 0, 0, 0}, + {0, 9, 8, 0, 0, 0, 0, 6, 0}, + {8, 0, 0, 0, 6, 0, 0, 0, 3}, + {4, 0, 0, 8, 0, 3, 0, 0, 1}, + {7, 0, 0, 0, 2, 0, 0, 0, 6}, + {0, 6, 0, 0, 0, 0, 2, 8, 0}, + {0, 0, 0, 4, 1, 9, 0, 0, 5}, + {0, 0, 0, 0, 8, 0, 0, 7, 9}, + } + SolveSudoku(input) - testPromising() + // testPromising() } From f93cc795fe4f402ac8e883b7fa7baa65cf532fbc Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 15:18:25 -0500 Subject: [PATCH 05/11] Implement remaining backtracking and pass given test case --- sudoku.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/sudoku.go b/sudoku.go index b94e003..89dca10 100644 --- a/sudoku.go +++ b/sudoku.go @@ -59,6 +59,10 @@ func FindNextUnfilled(board [][]int, curRow int, curCol int) (int, int) { // EFFECTS: Performs a (recursive) backtracking algorithm to solve the sudoku func SolveSudokuHelper(board [][]int, row int, col int) bool { // If sudoku is solved, return true + // All cells filled -> Solved! + if row < 0 { + return true + } // Otherwise // Try to place number 1 through 9 at current row and col for num := 1; num <= 9; num++ { @@ -68,13 +72,13 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { board[row][col] = num // Recursion with the next unfilled cell on the board nextRow, nextCol := FindNextUnfilled(board, row, col) - fmt.Printf("Next row: %d, Next col: %d\n", nextRow, nextCol) - // All cells filled -> Solved! - if nextRow < 0 { + // If recursive function call returned true + if SolveSudokuHelper(board, nextRow, nextCol) { + // then it means puzzle solved so return true return true } - // If recursive function call returned true, then it means puzzle solved so return true - // Otherwise, Backtrack: "unplace" the number at current cell + // Otherwise, Backtrack: "unplace" the number at current cell + board[row][col] = 0 } } // Reaching this point means that none of the numbers from 1 to 9 can be placed at current cell @@ -82,10 +86,10 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { return false } -// Requires: The input grid will be a 9x9 two-dimensional array of integers. +// REQUIRES: The input grid will be a 9x9 two-dimensional array of integers. // The input grid will have exactly one solution. -// Modifies: N/A -// Effects: Solves the sudoku, returns a 9 by 9 array of the solved sudoku +// MODIFIES: N/A +// EFFECTS: Solves the sudoku, returns a 9 by 9 array of the solved sudoku func SolveSudoku(sudoku [][]int) [][]int { // for storing the index of the first unfilled cell row := -1 @@ -155,5 +159,12 @@ func main() { } SolveSudoku(input) + for i := 0; i < len(input); i++ { + for j := 0; j < len(input[i]); j++ { + fmt.Printf("%d ", input[i][j]) + } + fmt.Println() + } + // testPromising() } From 77f68c3c014b601710b469a26c2baf40fa177d11 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 15:30:01 -0500 Subject: [PATCH 06/11] Write some edge case tests --- sudoku_test.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/sudoku_test.go b/sudoku_test.go index 63e3196..ecd30ee 100644 --- a/sudoku_test.go +++ b/sudoku_test.go @@ -5,6 +5,131 @@ import ( "testing" ) +// Test: sudoku is already solved +func TestAllFilled(t *testing.T) { + input := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + expected := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + solved := SolveSudoku(input) + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + + +// Test: exactly 1 cell is 0 +func TestOneEmptyCell(t *testing.T) { + input := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 0, 3, 7, 9, 1}, // an empty cell here + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + expected := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + solved := SolveSudoku(input) + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + +// Test: exactly 1 cell in each row, col, grid is 0 +func TestNineEmptyCells(t *testing.T) { + input := [][]int{ + {0, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 0, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 0}, + {8, 5, 9, 0, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 0, 1}, + {7, 0, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 0, 8, 4}, + {2, 8, 0, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 0, 1, 7, 9}, + } + expected := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + solved := SolveSudoku(input) + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + +// Test: unfilled cells are not represented by 0 +func TestWeirdNumbers(t *testing.T) { + input := [][]int{ + {5, 3, -1, 0, 7, 0, 0, 0, 10}, + {6, 0, 0, 1, 9, 5, 0, 10, 50}, + {0, 9, 8, -90, 0, 0, 0, 6, 0}, + {8, 0, 500, 0, 6, 0, 0, 0, 3}, + {4, 20, 0, 8, 0, 3, 0, 10, 1}, + {7, 0, -1, 0, 2, 0, 79, 0, 6}, + {0, 6, 0, 19, 0, 0, 2, 8, -1}, + {45, 0, 0, 4, 1, 9, 0, -1, 5}, + {-10, 0, 0, 0, 8, 0, 0, 7, 9}, + } + + expected := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + + solved := SolveSudoku(input) + + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + +// Given test case func TestSolveSudoku(t *testing.T) { input := [][]int{ {5, 3, 0, 0, 7, 0, 0, 0, 0}, From f8f39b7a856368c534f032d178447055c2ea086e Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 15:35:13 -0500 Subject: [PATCH 07/11] Clean up code and improve comment consistency --- sudoku.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/sudoku.go b/sudoku.go index 89dca10..3eca854 100644 --- a/sudoku.go +++ b/sudoku.go @@ -2,22 +2,21 @@ package main import "fmt" -// REQUIRES: existing board is valid +// REQUIRES: Existing board is valid // 0 <= row, col <= 8; board[row][col] is unfilled // 1 <= num <= 9 // MODIFIES: N/A // EFFECTS: Determines if placing num at board[row][col] is valid func Promising(board [][]int, row int, col int, num int) bool { - // check all cells on the same row and same column as the cell at [row][col] + // Check all cells on the same row or same column as the cell at [row][col] for i := 0; i < 9; i++ { if board[row][i] == num || board[i][col] == num { return false } } - // check all cells in the same 3 by 3 grid as the cell at [row][col] + // Check all cells in the same 3 by 3 grid as the cell at [row][col] startRowIdx := (row / 3) * 3 startColIdx := (col / 3) * 3 - // fmt.Printf("Start row idx: %d, Start col idx: %d\n", startRowIdx, startColIdx) for i := startRowIdx; i < startRowIdx + 3; i++ { for j := startColIdx; j < startColIdx + 3; j++ { if board[i][j] == num { @@ -58,13 +57,12 @@ func FindNextUnfilled(board [][]int, curRow int, curCol int) (int, int) { // MODIFIES: board // EFFECTS: Performs a (recursive) backtracking algorithm to solve the sudoku func SolveSudokuHelper(board [][]int, row int, col int) bool { - // If sudoku is solved, return true - // All cells filled -> Solved! + // If sudoku is solved, i.e. all cells have been filled if row < 0 { return true } // Otherwise - // Try to place number 1 through 9 at current row and col + // Try to place number 1 through 9 at current [rol][col] for num := 1; num <= 9; num++ { // Check constraint: If placing this number does not violate any rules of sudoku if Promising(board, row, col, num) { @@ -74,7 +72,7 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { nextRow, nextCol := FindNextUnfilled(board, row, col) // If recursive function call returned true if SolveSudokuHelper(board, nextRow, nextCol) { - // then it means puzzle solved so return true + // Then it means puzzle solved return true } // Otherwise, Backtrack: "unplace" the number at current cell @@ -91,7 +89,7 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { // MODIFIES: N/A // EFFECTS: Solves the sudoku, returns a 9 by 9 array of the solved sudoku func SolveSudoku(sudoku [][]int) [][]int { - // for storing the index of the first unfilled cell + // For storing the index of the first unfilled cell row := -1 col := -1 for i := 0; i < len(sudoku); i++ { @@ -107,12 +105,13 @@ func SolveSudoku(sudoku [][]int) [][]int { } } } - // calls helper function which does the backtracking - // pass sudoku 2d array by reference + // Calls helper function which does the backtracking + // Pass sudoku 2d array by reference SolveSudokuHelper(sudoku, row, col) return sudoku } +// Unit test for Promising function func testPromising() { input := [][]int{ {5, 3, 0, 0, 7, 0, 0, 0, 0}, @@ -165,6 +164,4 @@ func main() { } fmt.Println() } - - // testPromising() } From 0d0a4c81c61cc7296e148d41d7f5bc2e464e14d7 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 16:10:58 -0500 Subject: [PATCH 08/11] Use memoization to improve constraint checking time complexity --- sudoku.go | 100 +++++++++++++++++++++--------------------------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/sudoku.go b/sudoku.go index 3eca854..c9c47f2 100644 --- a/sudoku.go +++ b/sudoku.go @@ -2,29 +2,17 @@ package main import "fmt" -// REQUIRES: Existing board is valid -// 0 <= row, col <= 8; board[row][col] is unfilled +// REQUIRES: memoRow, memoCol, memoGrid accurately reflect the current board +// 0 <= row, col <= 8 +// memo corresponding to [row][col] is currently not updated (false) // 1 <= num <= 9 // MODIFIES: N/A // EFFECTS: Determines if placing num at board[row][col] is valid -func Promising(board [][]int, row int, col int, num int) bool { - // Check all cells on the same row or same column as the cell at [row][col] - for i := 0; i < 9; i++ { - if board[row][i] == num || board[i][col] == num { - return false - } - } - // Check all cells in the same 3 by 3 grid as the cell at [row][col] - startRowIdx := (row / 3) * 3 - startColIdx := (col / 3) * 3 - for i := startRowIdx; i < startRowIdx + 3; i++ { - for j := startColIdx; j < startColIdx + 3; j++ { - if board[i][j] == num { - return false - } - } - } - return true +func Promising(memoRow [9][10]bool, memoCol [9][10]bool, + memoGrid [3][3][10]bool,row int, col int, num int) bool { + // Return true iff num does not exist in the row, column, or grid + // which cell [row][col] corresponds to + return !memoRow[row][num] && !memoCol[col][num] && !memoGrid[row/3][col/3][num] } // REQUIRES: Existing board is valid @@ -53,10 +41,12 @@ func FindNextUnfilled(board [][]int, curRow int, curCol int) (int, int) { } // REQUIRES: board is a 2d array representing a sudoku with exactly 1 solution +// memoRow, memoCol, memoGrid accurately reflect the current board // 0 <= row, col <= 8 // MODIFIES: board // EFFECTS: Performs a (recursive) backtracking algorithm to solve the sudoku -func SolveSudokuHelper(board [][]int, row int, col int) bool { +func SolveSudokuHelper(board [][]int, memoRow [9][10]bool, memoCol [9][10]bool, + memoGrid [3][3][10]bool, row int, col int) bool { // If sudoku is solved, i.e. all cells have been filled if row < 0 { return true @@ -65,18 +55,24 @@ func SolveSudokuHelper(board [][]int, row int, col int) bool { // Try to place number 1 through 9 at current [rol][col] for num := 1; num <= 9; num++ { // Check constraint: If placing this number does not violate any rules of sudoku - if Promising(board, row, col, num) { - // Place number + if Promising(memoRow, memoCol, memoGrid, row, col, num) { + // Place number & update memos board[row][col] = num + memoRow[row][num] = true + memoCol[col][num] = true + memoGrid[row/3][col/3][num] = true // Recursion with the next unfilled cell on the board nextRow, nextCol := FindNextUnfilled(board, row, col) // If recursive function call returned true - if SolveSudokuHelper(board, nextRow, nextCol) { + if SolveSudokuHelper(board, memoRow, memoCol, memoGrid, nextRow, nextCol) { // Then it means puzzle solved return true } // Otherwise, Backtrack: "unplace" the number at current cell board[row][col] = 0 + memoRow[row][num] = false + memoCol[col][num] = false + memoGrid[row/3][col/3][num] = false } } // Reaching this point means that none of the numbers from 1 to 9 can be placed at current cell @@ -92,58 +88,38 @@ func SolveSudoku(sudoku [][]int) [][]int { // For storing the index of the first unfilled cell row := -1 col := -1 - for i := 0; i < len(sudoku); i++ { - for j := 0; j < len(sudoku[i]); j++ { + // For storing whether a number in a row, col, grid is already placed + // All elements in array automatically initialized to false + var rowsMemo, colsMemo [9][10]bool + var gridsMemo [3][3][10]bool + + for i := 0; i < 9; i++ { + for j := 0; j < 9; j++ { + // Get number at current cell + num := sudoku[i][j] // Preprocess the board by filling all unfilled values with 0 - if sudoku[i][j] < 1 || sudoku[i][j] > 9 { + if num < 1 || num > 9 { sudoku[i][j] = 0 // Find the index of the first unfilled cell if row == -1 { row = i col = j } + // Preprocess which nums already exist in current row/col/grid + } else { + // Update memo to true to indicate that num exists in this row/col/grid + rowsMemo[i][num] = true + colsMemo[j][num] = true + gridsMemo[i/3][j/3][num] = true } } } // Calls helper function which does the backtracking - // Pass sudoku 2d array by reference - SolveSudokuHelper(sudoku, row, col) + // Pass sudoku 2d array and 3 memos by reference + SolveSudokuHelper(sudoku, rowsMemo, colsMemo, gridsMemo, row, col) return sudoku } -// Unit test for Promising function -func testPromising() { - input := [][]int{ - {5, 3, 0, 0, 7, 0, 0, 0, 0}, - {6, 0, 0, 1, 9, 5, 0, 0, 0}, - {0, 9, 8, 0, 0, 0, 0, 6, 0}, - {8, 0, 0, 0, 6, 0, 0, 0, 3}, - {4, 0, 0, 8, 0, 3, 0, 0, 1}, - {7, 0, 0, 0, 2, 0, 0, 0, 6}, - {0, 6, 0, 0, 0, 0, 2, 8, 0}, - {0, 0, 0, 4, 1, 9, 0, 0, 5}, - {0, 0, 0, 0, 8, 0, 0, 7, 9}, - } - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 5)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 7)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 4)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 9)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 3)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 8)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 2, 0, 6)) - fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 2, 0, 1)) - fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 2, 0, 2)) - - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 7)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 9)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 7, 6, 8)) - - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 8, 5, 4)) - fmt.Printf("Expected: %t; Got: %t\n", false, Promising(input, 8, 5, 1)) - fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 8, 5, 6)) - fmt.Printf("Expected: %t; Got: %t\n", true, Promising(input, 8, 5, 2)) -} - func main() { input := [][]int{ {5, 3, 0, 0, 7, 0, 0, 0, 0}, From 313da52056657d27366e42770e143578b6de4dc4 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 16:30:56 -0500 Subject: [PATCH 09/11] Write more tests and pass all --- sudoku_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/sudoku_test.go b/sudoku_test.go index ecd30ee..0eee3a1 100644 --- a/sudoku_test.go +++ b/sudoku_test.go @@ -96,6 +96,66 @@ func TestNineEmptyCells(t *testing.T) { } } +// Test: exactly 2 cells empty in each grid +func TestSomeEmptyCells(t *testing.T) { + input := [][]int{ + {5, 0, 4, 6, 7, 0, 9, 1, 0}, + {6, 7, 2, 1, 0, 5, 3, 0, 8}, + {0, 9, 8, 0, 4, 2, 5, 6, 7}, + {8, 5, 0, 7, 6, 1, 0, 2, 3}, + {4, 2, 6, 8, 0, 3, 7, 9, 1}, + {0, 1, 3, 0, 2, 4, 8, 5, 0}, + {9, 6, 1, 5, 0, 7, 0, 8, 4}, + {2, 0, 7, 4, 1, 0, 6, 3, 5}, + {0, 4, 5, 2, 8, 6, 1, 0, 9}, + } + expected := [][]int{ + {5, 3, 4, 6, 7, 8, 9, 1, 2}, + {6, 7, 2, 1, 9, 5, 3, 4, 8}, + {1, 9, 8, 3, 4, 2, 5, 6, 7}, + {8, 5, 9, 7, 6, 1, 4, 2, 3}, + {4, 2, 6, 8, 5, 3, 7, 9, 1}, + {7, 1, 3, 9, 2, 4, 8, 5, 6}, + {9, 6, 1, 5, 3, 7, 2, 8, 4}, + {2, 8, 7, 4, 1, 9, 6, 3, 5}, + {3, 4, 5, 2, 8, 6, 1, 7, 9}, + } + solved := SolveSudoku(input) + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + +// Test: More empty cells but still one single solution +func TestMoreEmptyCells(t *testing.T) { + input := [][]int{ + {0, 0, 2, 0, 3, 0, 0, 0, 8}, + {0, 0, 0, 0, 0, 8, 0, 0, 0}, + {0, 3, 1, 0, 2, 0, 0, 0, 0}, + {0, 6, 0, 0, 5, 0, 2, 7, 0}, + {0, 1, 0, 0, 0, 0, 0, 5, 0}, + {2, 0, 4, 0, 6, 0, 0, 3, 1}, + {0, 0, 0, 0, 8, 0, 6, 0, 5}, + {0, 0, 0, 0, 0, 0, 0, 1, 3}, + {0, 0, 5, 3, 1, 0, 4, 0, 0}, + } + expected := [][]int{ + {6, 7, 2, 4, 3, 5, 1, 9, 8}, + {5, 4, 9, 1, 7, 8, 3, 6, 2}, + {8, 3, 1, 6, 2, 9, 5, 4, 7}, + {3, 6, 8, 9, 5, 1, 2, 7, 4}, + {9, 1, 7, 2, 4, 3, 8, 5, 6}, + {2, 5, 4, 8, 6, 7, 9, 3, 1}, + {1, 9, 3, 7, 8, 4, 6, 2, 5}, + {4, 8, 6, 5, 9, 2, 7, 1, 3}, + {7, 2, 5, 3, 1, 6, 4, 8, 9}, + } + solved := SolveSudoku(input) + if !reflect.DeepEqual(solved, expected) { + t.Errorf("Sudoku puzzle was not solved correctly. Expected:\n%v\n\nGot:\n%v", expected, solved) + } +} + // Test: unfilled cells are not represented by 0 func TestWeirdNumbers(t *testing.T) { input := [][]int{ From b0b41204eebbf410dc7a7a2f529c25ee5f1a21d3 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 17:56:43 -0500 Subject: [PATCH 10/11] Clean up unnecessary code --- sudoku.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/sudoku.go b/sudoku.go index c9c47f2..d7b9263 100644 --- a/sudoku.go +++ b/sudoku.go @@ -120,23 +120,13 @@ func SolveSudoku(sudoku [][]int) [][]int { return sudoku } -func main() { - input := [][]int{ - {5, 3, 0, 0, 7, 0, 0, 0, 0}, - {6, 0, 0, 1, 9, 5, 0, 0, 0}, - {0, 9, 8, 0, 0, 0, 0, 6, 0}, - {8, 0, 0, 0, 6, 0, 0, 0, 3}, - {4, 0, 0, 8, 0, 3, 0, 0, 1}, - {7, 0, 0, 0, 2, 0, 0, 0, 6}, - {0, 6, 0, 0, 0, 0, 2, 8, 0}, - {0, 0, 0, 4, 1, 9, 0, 0, 5}, - {0, 0, 0, 0, 8, 0, 0, 7, 9}, - } - SolveSudoku(input) - - for i := 0; i < len(input); i++ { - for j := 0; j < len(input[i]); j++ { - fmt.Printf("%d ", input[i][j]) +// REQUIRES: The input grid will be a 9x9 two-dimensional array of integers. +// MODIFIES: N/A +// EFFECTS: Prints the sudoku board +func PrintSudoku(sudoku [][]int) { + for i := 0; i < len(sudoku); i++ { + for j := 0; j < len(sudoku[i]); j++ { + fmt.Printf("%d ", sudoku[i][j]) } fmt.Println() } From 4cf9daf3b91d7d5f32009da2789390c13d5af027 Mon Sep 17 00:00:00 2001 From: zhaojer Date: Sun, 21 Jan 2024 17:57:15 -0500 Subject: [PATCH 11/11] Write documentation for the program --- README.md | 84 ++++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 9be54f0..294313b 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,53 @@ -# Technical Instructions -1. Fork this repo to your local Github account. -2. Create a new branch to complete all your work in. -3. Test your work using the provided tests -4. Create a Pull Request against the Shopify Main branch when you're done and all tests are passing - # Shopify Intern Assessment Production Engineering ## Description -Write a Go program that solves a given Sudoku puzzle. The program should take a 9x9 grid as input, where empty cells are represented by zeros (0), and output the solved Sudoku grid. +This is a Go program that solves a given Sudoku puzzle. The program takes a 9x9 grid as input, where empty cells may be represented by zeros (0), and output the solved Sudoku grid (see [Constraints](#constraints) section for more info). A Sudoku puzzle is a 9x9 grid divided into nine 3x3 sub-grids. The goal is to fill in the empty cells with numbers from 1 to 9, such that each row, each column, and each sub-grid contains all the numbers from 1 to 9 without repetition. -Your program should implement an efficient algorithm to solve the Sudoku puzzle and print the solved grid to the console. - -### Example: Input: +## My Implementation +My implementation (see `sudoku.go`) uses a typical serial backtracking algorithm combined with memoization to efficiently solve a given 9 by 9 sudoku. + +The motivation behind using a backtracking algorithm for this problem is because it is easy to implement and highly efficient in solving sudokus, perfectly matching what the Go language is known for: its simplicity and efficiency. Though Go is also known for its concurrency, from my experience, implementing a parallel algorithm to solve a 9 by 9 sudoku is not only complicated due to the inherent data dependencies, it also may not result in any runtime improvement at all since the synchronization and communication overheads introduced in a parallel program may outweigh the performance gain when the input size is small. + +I also took adavantage of the given constraints in the problem statement that the input sudoku is guaranteed to be 9 by 9 and have exactly 1 solution, by adding memoization to improve the runtime of the constraint-checking function, which determines whether placing a number at an unfilled cell would still result in a valid sudoku. By doing so, the constraint-checking function only needs 1 boolean statement, as compared to a naive for loop. Despite both implementation being theoretically O(1) given a fixed-size input, practically, the memoization approach did result in some minor speedup (< 1 ms difference when tested with a difficult sudoku), therefore it is kept in the final implementation. + +For more low-level details on how the backtracking & memoization worked (e.g. specific functions definitions, variables, data structures, etc.), see `sudoku.go`. + +### Test Cases +More test cases are added in `sudoku_test.go`, which tests some edge cases such as the given sudoku is already completed, everything is filled in the sudoku except 1 cell, etc. + +## Running the Program +To run the tests, open project root directory (e.g. `infra-intern-assessment`) in terminal, run `go test`. + +Or, you can do the following to solve a custome sudoku: +1. Add a main function in `sudoku.go` +2. Make a sudoku of your choice in the form of 2d array +3. Call `SolveSudoku` function with the sudoku you just made +```Go +// Example main function with a user-defined sudoku +func main() { + mySudoku := [][]int{ + {5, 3, 0, 0, 7, 0, 0, 0, 0}, + {6, 0, 0, 1, 9, 5, 0, 0, 0}, + {0, 9, 8, 0, 0, 0, 0, 6, 0}, + {8, 0, 0, 0, 6, 0, 0, 0, 3}, + {4, 0, 0, 8, 0, 3, 0, 0, 1}, + {7, 0, 0, 0, 2, 0, 0, 0, 6}, + {0, 6, 0, 0, 0, 0, 2, 8, 0}, + {0, 0, 0, 4, 1, 9, 0, 0, 5}, + {0, 0, 0, 0, 8, 0, 0, 7, 9}, + } + // call function that solves the sudoku + SolveSudoku(mySudoku) + // print result + PrintSudoku(mySudoku) +} ``` -[ - [5, 3, 0, 0, 7, 0, 0, 0, 0], - [6, 0, 0, 1, 9, 5, 0, 0, 0], - [0, 9, 8, 0, 0, 0, 0, 6, 0], - [8, 0, 0, 0, 6, 0, 0, 0, 3], - [4, 0, 0, 8, 0, 3, 0, 0, 1], - [7, 0, 0, 0, 2, 0, 0, 0, 6], - [0, 6, 0, 0, 0, 0, 2, 8, 0], - [0, 0, 0, 4, 1, 9, 0, 0, 5], - [0, 0, 0, 0, 8, 0, 0, 7, 9] -] -``` - -### Program Output: -``` -[ - [5, 3, 4, 6, 7, 8, 9, 1, 2], - [6, 7, 2, 1, 9, 5, 3, 4, 8], - [1, 9, 8, 3, 4, 2, 5, 6, 7], - [8, 5, 9, 7, 6, 1, 4, 2, 3], - [4, 2, 6, 8, 5, 3, 7, 9, 1], - [7, 1, 3, 9, 2, 4, 8, 5, 6], - [9, 6, 1, 5, 3, 7, 2, 8, 4], - [2, 8, 7, 4, 1, 9, 6, 3, 5], - [3, 4, 5, 2, 8, 6, 1, 7, 9] -] -``` - -## Instructions: -1. Write a function called SolveSudoku that takes a 9x9 grid as input and returns the solved Sudoku grid. -2. Implement an efficient algorithm to solve the Sudoku puzzle. You can use any approach or technique you prefer. -3. Confirm the validity of your code against the tests found in this repo. -4. Ensure that your code is well-documented and easy to understand. ## Constraints: - The input grid will be a 9x9 two-dimensional array of integers. - The input grid will have exactly one solution. - The input grid may contain zeros (0) to represent empty cells. -## Validation: -To validate the correctness of the solution, you can compare the output of the program with the expected output for a set of test cases containing unsolved Sudoku puzzles.