简介

N皇后问题分析:

  • 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n (n-1) …. * 1。
  • 空间复杂度:O(n),和子集问题同理。

解数独问题分析:

  • 时间复杂度:O(9^m) , m是’.’的数目。
  • 空间复杂度:O(n^2),递归的深度是n^2

    51. N 皇后

    题目描述

    image.png

    解题思路

  • 递归函数参数

我依然是定义全局变量二维数组result来记录最终结果。
参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。

  • 递归终止条件:

可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。

  • 单层搜索的逻辑

递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。
每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
验证棋盘是否合法
按照如下标准去重:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线 (45度和135度角)

image.png

  1. public List<List<String>> solveNQueens(int n) {
  2. if (n == 0) {
  3. return res;
  4. }
  5. // 需要将二维数组全部赋值,防止出现空字符
  6. char[][] chars = new char[n][n];
  7. for (char[] num : chars) {
  8. Arrays.fill(num, '.'); // 将指定的char值分配给指定的char数组的每个元素
  9. }
  10. backtracking(n, 0, chars);
  11. return res;
  12. }
  13. List<List<String>> res = new ArrayList<>();*/
  14. /**
  15. * 回溯搜索
  16. *
  17. * @param n 表示输入的棋盘大小
  18. * @param row 表示当前遍历到第几行了
  19. * @param chessboard 表示棋盘( ‘.’ 表示空格,‘Q’表示皇后)
  20. */
  21. public void backtracking(int n, int row, char[][] chessboard) {
  22. if (row == n) {
  23. res.add(numToList(chessboard));// 此行
  24. return;
  25. }
  26. for (int col = 0; col < n; col++) { // for遍历每列
  27. if (isValid(row, col, n, chessboard)) {
  28. chessboard[row][col] = 'Q'; // 放置皇后
  29. backtracking(n, row + 1, chessboard); // 递归遍历每行,注意row需要加一
  30. chessboard[row][col] = '.'; // 回溯
  31. }
  32. }
  33. }
  34. /**
  35. * 二维字符数组转化为List集合
  36. *
  37. * @param chessboard
  38. * @return
  39. */
  40. public List<String> numToList(char[][] chessboard) {
  41. List<String> list = new ArrayList<>();
  42. for (char[] num : chessboard) {
  43. list.add(String.valueOf(num));
  44. }
  45. return list;
  46. }
  47. /**
  48. * 判断是否满足n皇后的条件
  49. *
  50. * @param row
  51. * @param col
  52. * @param chessboard
  53. * @return
  54. */
  55. public boolean isValid(int row, int col, int n, char[][] chessboard) {
  56. // 判断一列一列是否有皇后
  57. for (int i = 0; i < row; i++) {
  58. if (chessboard[i][col] == 'Q')
  59. return false;
  60. }
  61. // 判断135度是否有皇后
  62. for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
  63. if (chessboard[i][j] == 'Q')
  64. return false;
  65. }
  66. // 判断45度是否有皇后
  67. for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
  68. if (chessboard[i][j] == 'Q')
  69. return false;
  70. }
  71. return true;
  72. }

在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢?
因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

52. N皇后 II

题目描述

image.png

解题思路

和上一题求解方式一直,但取结果时只取多少种。

  1. class Solution {
  2. public int totalNQueens(int n) {
  3. char[][] chessboard = new char[n][n];
  4. for (int i = 0; i < n; i++) {
  5. Arrays.fill(chessboard[i], '.');
  6. }
  7. backtracking(n, 0, chessboard);
  8. return result;
  9. }
  10. int result = 0;
  11. public boolean backtracking(int n, int row, char[][] chessboard) {
  12. if (row == n) {
  13. result++;
  14. return true;
  15. }
  16. for (int i = 0; i < n; i++) {
  17. if (isValid(chessboard, n, row, i)) {
  18. chessboard[row][i] = 'Q';
  19. backtracking(n, row + 1, chessboard);
  20. chessboard[row][i] = '.';
  21. }
  22. }
  23. return false;
  24. }
  25. public boolean isValid(char[][] chessboard, int n, int row, int col) {
  26. int i = 0, j = 0;
  27. for (i = 0; i < row; i++) {
  28. if (chessboard[i][col] == 'Q') {
  29. return false;
  30. }
  31. }
  32. for (i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
  33. if (chessboard[i][j] == 'Q') {
  34. return false;
  35. }
  36. }
  37. for (i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
  38. if (chessboard[i][j] == 'Q') {
  39. return false;
  40. }
  41. }
  42. return true;
  43. }
  44. }

37. 解数独

题目描述

image.png

解题思路

N皇后问题是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。
本题就不一样了,本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深

  • 递归函数以及参数

递归函数的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,这一点在N皇后问题中已经介绍过了,一样的道理。

  • 递归终止条件

本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
不用终止条件会不会死循环?
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
那么有没有永远填不满的情况呢?
这个问题我在递归单层搜索逻辑里在来讲!

  • 递归单层搜索逻辑

在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!如果9个数字都不能填充进去,那么返回false,这就是没用终止条件也不会永远填不满棋盘而无限递归下去!
判断棋盘是否合法
判断棋盘是否合法有如下三个维度:

  • 同行是否重复
  • 同列是否重复
  • 9宫格里是否重复

image.png

  1. public void solveSudoku(char[][] board) {
  2. if (board == null) {
  3. return;
  4. }
  5. backtracking(board);
  6. }
  7. public boolean backtracking(char[][] board) {
  8. // 可以不需要返回条件,不符合会返回false
  9. // 遍历整个二维数组
  10. for (int i = 0; i < board.length; i++) {
  11. for (int j = 0; j < board[0].length; j++) {
  12. if (board[i][j] != '.') continue; // 此时位置已经有数字,跳过
  13. for (char k = '1'; k <= '9'; k++) { // 依次判断数组 1~9
  14. if (isValid(i, j, k, board)) {
  15. board[i][j] = k;
  16. if (backtracking(board)) return true; // 找到一条就返回
  17. board[i][j] = '.';
  18. }
  19. }
  20. return false; // 如果9个数字都不满足条件,那么此处此数独无解
  21. }
  22. }
  23. return true; // 所有都遍历完没有返回false,满足条件
  24. }
  25. /**
  26. * 判断一行,一列,以及九宫格中是否有重复的数字
  27. *
  28. * @param row
  29. * @param col
  30. * @param val
  31. * @param board
  32. * @return
  33. */
  34. public boolean isValid(int row, int col, char val, char[][] board) {
  35. // 判断一行
  36. for (int i = 0; i < board[row].length; i++) {
  37. if (board[row][i] == val) {
  38. return false;
  39. }
  40. }
  41. // 判断一列
  42. for (int i = 0; i < board.length; i++) {
  43. if (board[i][col] == val) {
  44. return false;
  45. }
  46. }
  47. // 判断九宫格
  48. int startRow = (row / 3) * 3; // 行起始位置
  49. int startCol = (col / 3) * 3; // 列起始位置
  50. for (int i = startRow; i < startRow + 3; i++) {
  51. for (int j = startCol; j < startCol + 3; j++) {
  52. if (board[i][j] == val) {
  53. return false;
  54. }
  55. }
  56. }
  57. return true;
  58. }