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]);
		}
	}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值