Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete Shopify infra intern OA #77

Closed
wants to merge 12 commits into from
84 changes: 39 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
132 changes: 132 additions & 0 deletions sudoku.go
Original file line number Diff line number Diff line change
@@ -1 +1,133 @@
package main

import "fmt"

// 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(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
// 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
// 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, 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
}
// Otherwise
// 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(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, 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
// 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
// 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 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 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 and 3 memos by reference
SolveSudokuHelper(sudoku, rowsMemo, colsMemo, gridsMemo, row, col)
return sudoku
}

// 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()
}
}
Loading
Loading