LeetCode - Hard - 37. Sudoku Solver

Topic

  • Backtracking

Description

https://leetcode.com/problems/sudoku-solver/

Write a program to solve a Sudoku puzzle by filling the empty cells.

A sudoku solution must satisfy all of the following rules:

  1. Each of the digits 1-9 must occur exactly once in each row.
  2. Each of the digits 1-9 must occur exactly once in each column.
  3. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.

The '.' character indicates empty cells.

Example 1:

Input: board = 
[["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]]
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"]]
Explanation: The input board is shown above and the only valid solution is shown below:

在這裏插入圖片描述

Constraints:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] is a digit or '.'.
  • It is guaranteed that the input board has only one solution.

Analysis

思路分析

數獨問題可以使用回溯法暴力搜索,用到二維遞歸,本題中數獨每一個空格位置都要放一個數字,並檢查數字是否合法。本題的樹形圖如下:

驗證合法

判斷數字放入數獨終是否合法有如下三個維度:

  • 同行是否重複
  • 同列是否重複
  • 所屬9宮格里是否重複

代碼如下:

private boolean isValid(char[][] board, int row, int col, int num) {
	// 所在行
	for (int i = 0; i < 9; i++) {
		if (board[row][i] == num)
			return false;
	}

	// 所在列
	for (int i = 0; i < 9; i++) {
		if (board[i][col] == num)
			return false;
	}

	// 所屬9格
	int startX = row / 3 * 3;
	int startY = col / 3 * 3;

	for (int i = startX; i < startX + 3; i++) {
		for (int j = startY; j < startY + 3; j++) {
			if (board[i][j] == num) {
				return false;
			}
		}
	}

	return true;
}

回溯三弄

函數簽名

因爲解數獨找到一個符合的條件(就在樹的葉子節點上)立刻就返回,相當於找從根節點到葉子節點一條唯一路徑,所以需要使用boolean返回值。

代碼如下:

private boolean backtracking(char[][] board) {}

終止條件

本題遞歸不用終止條件,解數獨是要遍歷整個樹形結構尋找可能的葉子節點就立刻返回。

不用終止條件會不會死循環?遞歸的下一層的數獨盤一定比上一層的數獨盤多一個數,等數填滿了數獨盤自然就終止(填滿當然好了,說明找到結果了),所以不需要終止條件。

遍歷順序

思路分析樹形圖中可以看出我們需要的是一個二維遞歸(也就是兩個for循環嵌套着遞歸)

一個for循環遍歷數獨盤的行,一個for循環遍歷數獨盤的列,一行一列確定下來之後,遞歸遍歷這個位置放9個數字的可能性。

代碼如下:

private boolean backtracking(char[][] board) {

	for (int i = 0; i < 9; i++) {
		for (int j = 0; j < 9; j++) {
			if (board[i][j] != '.')
				continue;

			for (char k = '1'; k <= '9'; k++) {
				if (isValid(board, i, j, k)) {
					board[i][j] = k;
					if (backtracking(board))//
						return true;
					board[i][j] = '.';
				}
			}
			return false;// 9個數都試完了,都不行,那麼就返回false
		}
	}

	return true;// 遍歷完沒有返回false,說明找到了合適數獨盤位置了
}

注意這裏return false的地方,這裏放return false 是有講究的。

因爲如果一行一列確定下來了,這裏嘗試了9個數都不行,說明這個數獨盤找不到解決數獨問題的解!

那麼會直接返回, 這也就是爲什麼沒有終止條件也不會永遠填不滿數獨盤而無限遞歸下去!

降維打擊

其實我們可以將二維數組降維至一維回溯的,代碼如下:

public void solveSudoku2(char[][] board) {
	backtracking2(board, 0, 0);
}

private boolean backtracking2(char[][] board, int i, int j) {
	if (i == 9)
		return true;
	if (j == 9)
		return backtracking2(board, i + 1, 0);//換下一行遍歷
	if (board[i][j] != '.')
		return backtracking2(board, i, j + 1);

	for (char c = '1'; c <= '9'; c++) {//
		if (isValid(board, i, j, c)) {
			board[i][j] = c;
			if (backtracking2(board, i, j + 1))
				return true;
			board[i][j] = '.';
		}
	}

	return false;
}

個人認爲本"降維打擊"方法更容易理解。

最終代碼

移步至Submission

參考資料

  1. Simple and Clean Solution / C++
  2. 回溯算法:解數獨

Submission

public class SudokuSolver {
	
	//方法一:回溯算法
	public void solveSudoku(char[][] board) {
		backtracking(board);
	}

	private boolean backtracking(char[][] board) {

		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				if (board[i][j] != '.')
					continue;

				for (char k = '1'; k <= '9'; k++) {
					if (isValid(board, i, j, k)) {
						board[i][j] = k;
						if (backtracking(board))//
							return true;
						board[i][j] = '.';
					}
				}
				return false;// 9個數都試完了,都不行,那麼就返回false
			}
		}

		return true;// 遍歷完沒有返回false,說明找到了合適數獨盤位置了
	}

	//方法二:回溯算法2,不同與方法一的二維遞歸,本方法把二維降維到一維
	//本人認爲本方法更容易理解
	public void solveSudoku2(char[][] board) {
		backtracking2(board, 0, 0);
	}

	private boolean backtracking2(char[][] board, int i, int j) {
		if (i == 9)
			return true;
		if (j == 9)
			return backtracking2(board, i + 1, 0);
		if (board[i][j] != '.')
			return backtracking2(board, i, j + 1);

		for (char c = '1'; c <= '9'; c++) {//
			if (isValid(board, i, j, c)) {
				board[i][j] = c;
				if (backtracking2(board, i, j + 1))
					return true;
				board[i][j] = '.';
			}
		}

		return false;
	}

	private boolean isValid(char[][] board, int row, int col, int num) {
		// 所在行
		for (int i = 0; i < 9; i++) {
			if (board[row][i] == num)
				return false;
		}

		// 所在列
		for (int i = 0; i < 9; i++) {
			if (board[i][col] == num)
				return false;
		}

		// 所屬9格
		int startX = row / 3 * 3;
		int startY = col / 3 * 3;

		for (int i = startX; i < startX + 3; i++) {
			for (int j = startY; j < startY + 3; j++) {
				if (board[i][j] == num) {
					return false;
				}
			}
		}

		return true;
	}
}

Test

import static org.junit.Assert.*;

import java.util.Arrays;

import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class SudokuSolverTest {

	private char[][] array;
	
	private static char[][] expected;
	
	private static SudokuSolver obj;
	
	@BeforeClass
	public static void initOnce() {
		obj = new SudokuSolver();
		
		expected = new char[][]{{'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'}};
	}
	
	@Before
	public void init() {
		array = new char[][]{{'5','3','.','.','7','.','.','.','.'},//
							{'6','.','.','1','9','5','.','.','.'},//
							{'.','9','8','.','.','.','.','6','.'},//
							{'8','.','.','.','6','.','.','.','3'},//
							{'4','.','.','8','.','3','.','.','1'},//
							{'7','.','.','.','2','.','.','.','6'},//
							{'.','6','.','.','.','.','2','8','.'},//
							{'.','.','.','4','1','9','.','.','5'},//
							{'.','.','.','.','8','.','.','7','9'}};
	}
	
	@Test
	public void test() {		
		obj.solveSudoku(array);
		//System.out.println(Arrays.deepToString(array));
		for(int i = 0; i < expected.length; i++) {
			assertArrayEquals(expected[i], array[i]);
		}
	}
	
	@Test
	public void test2() {		
		System.out.println(Arrays.deepToString(array));
		obj.solveSudoku2(array);
//		System.out.println(Arrays.deepToString(array));
		for(int i = 0; i < expected.length; i++) {
			assertArrayEquals(expected[i], array[i]);
		}
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章