刷题计划day26 回溯(五)回溯止【N 皇后】【解数独】

⚡刷题计划day26 回溯(五)继续,回溯最后一个专题,今天的是hard题,也是比较经典的题型,可以点个免费的赞哦~

往期可看专栏,关注不迷路,

您的支持是我的最大动力🌹~

目录

题目一:N 皇后

法一:简明版

回溯三部曲

法二:优化版

题目二:解数独

法一:简明版

回溯三部曲

法二:简明略优化版

法三:极致优化版


题目一:N 皇后

51. N 皇后

(https://leetcode.cn/problems/n-queens/description/)

n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二维矩阵还会有点不知所措。

首先来看一下皇后们的约束条件:

  1. 不能同行

  2. 不能同列

  3. 不能同斜线

确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。

下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:

从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了

法一:简明版

回溯三部曲

1.递归函数参数

我依然是定义全局变量二维数组result来记录最终结果。

参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。

代码如下:

List<List<String>> res = new ArrayList<>();
void backatracking(int n,int row,char[][] chessboard)

2.递归终止条件

当递归到叶子节点,就可以收集结果返回了。

if(n==row){res.add(Array2List(chessboard));return;
}

3.单层搜索逻辑

递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。

每次都是要从新的一行的起始位置开始搜,所以都是从0开始。

代码如下:

for(int i=0;i<n;i++){if(isValid(n,row,i,chessboard)){chessboard[row][i] = 'Q';backatracking(n,row+1,chessboard);chessboard[row][i] = '.';}
}

4.验证棋盘是否合法

按照如下标准去重:

  1. 不能同行

  2. 不能同列

  3. 不能同斜线 (45度和135度角)

代码如下:

public boolean isValid(int n,int row,int col,char[][] chessboard){// 检查列for(int i=row-1;i>=0;i--){if(chessboard[i][col]=='Q'){return false;}}// 检查45度对角线for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){if(chessboard[i][j]=='Q'){return false;}}// 检查135度对角线for(int i=row-1,j=col+1;i>=0 && j<n;i--,j++){if(chessboard[i][j]=='Q'){return false;}}return true;}

在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢?

因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了

完整代码:

class Solution {List<List<String>> res = new ArrayList<>();public List<List<String>> solveNQueens(int n) {char[][] chessboard = new char[n][n];for (char[] c : chessboard){Arrays.fill(c,'.');}backatracking(n,0,chessboard);return res;
​}public void backatracking(int n,int row,char[][] chessboard){if(n==row){res.add(Array2List(chessboard));return;}
​for(int i=0;i<n;i++){if(isValid(n,row,i,chessboard)){chessboard[row][i] = 'Q';backatracking(n,row+1,chessboard);chessboard[row][i] = '.';}}}
​public boolean isValid(int n,int row,int col,char[][] chessboard){// 检查列for(int i=row-1;i>=0;i--){if(chessboard[i][col]=='Q'){return false;}}// 检查45度对角线for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){if(chessboard[i][j]=='Q'){return false;}}// 检查135度对角线for(int i=row-1,j=col+1;i>=0 && j<n;i--,j++){if(chessboard[i][j]=='Q'){return false;}}return true;}
​public List<String> Array2List(char[][] chessboard){List<String> list = new ArrayList<>();
​for(char[] c : chessboard){list.add(new String(c));}return list;}
}

我们可以发现关于验证棋盘是否合法时,我们用了三个for循环,3n的复杂度,好像不是很好,那我们能不能使用O(1)的复杂度来解决呢,答案是可以的,见法二

法二:优化版

主要优化部分就是验证棋盘是否合法:

对于棋盘,有以下的性质:

1.对于对于 ↗ 方向的格子,行号加列号是不变的。

2.对于 ↖ 方向的格子,行号减列号是不变的。

可以结合下列棋盘理解一下:

所以我们就可以使用三个辅助数组来标记之前皇后所在的行号列号,如下:

boolean[] isColChoose = new boolean[n];//代表第c列是否被选
boolean[] r_diag = new boolean[2*n-1];//代表右对角线是否有元素,r+c
boolean[] l_diag = new boolean[2*n-1];//代表左对角线是否有元素,r-c。
注意r-c对应的数组下标可能为负数,所以我们后面判断需要将r-c加上n-1,使其从0开始。

完整代码如下:

class Solution {List<List<String>> res = new ArrayList<>();public List<List<String>> solveNQueens(int n) {char[][] chessboard = new char[n][n];boolean[] isColChoose = new boolean[n];//代表第c列是否被选boolean[] r_diag = new boolean[2*n-1];//代表右对角线是否有元素,r+cboolean[] l_diag = new boolean[2*n-1];//代表左对角线是否有元素,,r-cfor(char[] c : chessboard){Arrays.fill(c,'.');}bcaktracking(n,0,chessboard,isColChoose,r_diag,l_diag);return res;
​}public void bcaktracking(int n,int row,char[][] chessboard,boolean[] isColChoose,boolean[] r_diag,boolean[] l_diag){if(row==n){res.add(charToListString(chessboard));return;}
​for(int col=0;col<n;col++){if(!isColChoose[col] && !r_diag[row+col] && !l_diag[row-col+n-1]){chessboard[row][col]='Q';isColChoose[col]=true;r_diag[row+col]=true;l_diag[row-col+n-1]=true;bcaktracking(n,row+1,chessboard,isColChoose,r_diag,l_diag);//回溯chessboard[row][col]='.';isColChoose[col]=false;r_diag[row+col]=false;l_diag[row-col+n-1]=false;}}}
​
​public List<String> charToListString(char[][] chessboard){List<String> path = new ArrayList<>();for (char[] c : chessboard){path.add(String.copyValueOf(c));}return path;}

题目二:解数独

37. 解数独

(https://leetcode.cn/problems/sudoku-solver/description/)

本题建议先做一下n皇后问题,

本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深

因为这个树形结构太大了,我抽取一部分,如图所示:

法一:简明版

回溯三部曲

1.递归函数以及参数

递归函数的返回值需要是bool类型,为什么呢?

因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。

boolean backtracking(char[][] board)

2.递归终止条件

本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。

不用终止条件会不会死循环?

递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!

那么有没有永远填不满的情况呢?

这个问题我在递归单层搜索逻辑里再来讲!

3.递归单层搜索逻辑

在树形图中可以看出我们需要的是一个二维的递归 (一行一列)

一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!

代码如下,详见注释:

public boolean backtracking(char[][] board){for(int i=0;i<board.length;i++){// 遍历行for(int j=0;j<board[0].length;j++){// 遍历列if(board[i][j]!='.'){continue;}for(char k='1';k<='9';k++){if(isValid(i,j,k,board)){board[i][j]=k;// 放置kif(backtracking(board)){return true; // 如果找到合适一组立刻返回}board[i][j]='.';}}return false; // 9个数都试完了,都不行,那么就返回false}}return true;// 遍历完没有返回false,说明找到了合适棋盘位置了}

4.判断棋盘是否合法

判断棋盘是否合法有如下三个维度:

  • 同行是否重复

  • 同列是否重复

  • 9宫格里是否重复

代码如下:

public boolean isValid(int row,int col,char val,char[][] board){//行for(int i=0;i<9;i++){if(board[row][i]==val){return false;}}//列for(int j=0;j<9;j++){if(board[j][col]==val){return false;}}//九宫格int startRow = row/3*3;int startCol = col/3*3;for(int i=startRow;i<startRow+3;i++){for(int j=startCol;j<startCol+3;j++){if(board[i][j]==val){return false;}}}return true;
}

整体代码如下:

class Solution {public void solveSudoku(char[][] board) {backtracking(board);}public boolean backtracking(char[][] board){
​for(int i=0;i<board.length;i++){for(int j=0;j<board[0].length;j++){if(board[i][j]!='.'){continue;}for(char k='1';k<='9';k++){if(isValid(i,j,k,board)){board[i][j]=k;if(backtracking(board)){return true;}board[i][j]='.';}}return false;
​}}return true;}
​public boolean isValid(int row,int col,char val,char[][] board){//行for(int i=0;i<9;i++){if(board[row][i]==val){return false;}}//列for(int j=0;j<9;j++){if(board[j][col]==val){return false;}}//九宫格int startRow = row/3*3;int startCol = col/3*3;for(int i=startRow;i<startRow+3;i++){for(int j=startCol;j<startCol+3;j++){if(board[i][j]==val){return false;}}}return true;}
}

法二:简明略优化版

上面做了n皇后问题,很显然我们也可以使用之前法二的辅助数组来优化,直接详细见代码注释。

额外说一下关于九宫格的判断,可能很多同学开始很难理解为什么int k = row/3*3 + col/3;这样就可以判断是哪一个九宫格,网上也没有看到有题解有对其进行了较直观详细的解释,

可以理解理解这段话再加上图就很清晰了:

i / 3 得到行索引,然后乘以3是因为每个小格子有3列,这样可以得到当前行索引对应的小格子的起始列索引。然后,加上 j / 3 就可以得到当前元素在九宫格中的确切位置。比如以第一列的4,带进去算一下就明白了。

class Solution {public void solveSudoku(char[][] board) {boolean[][] isRow = new boolean[9][9];boolean[][] isCol = new boolean[9][9];boolean[][] isSquare9 = new boolean[9][9];//初始化for(int row=0;row<9;row++){for(int col=0;col<9;col++) {if(board[row][col]=='.'){continue;}int num = board[row][col]-'1';//数字从0开始,如果减0就是原数,但是构造二维数组就是new boolean[9][10];int k = row/3*3 + col/3;isRow[row][num]=true;isCol[col][num]=true;isSquare9[k][num]=true;//i / 3 得到行索引,然后乘以3是因为每个小格子有3列,这样可以得到当前行索引对应的小格子的起始列索引。然后,加上 j / 3 就可以得到当前元素在九宫格中的确切位置。}}
​backtracking(board,0,isRow,isCol,isSquare9);}public boolean backtracking(char[][] board,int n,boolean[][] isRow,boolean[][] isCol,boolean[][] isSquare9){
​if(n==81) return true;int i=n/9,j=n%9;if(board[i][j] != '.'){return backtracking(board,n+1,isRow,isCol,isSquare9);}int k = i/3*3+j/3;for(int num=0;num<9;num++){if(isRow[i][num] || isCol[j][num] || isSquare9[k][num]) continue;board[i][j] = (char) (num+'1');isRow[i][num]=isCol[j][num]=isSquare9[k][num]=true;if(backtracking(board,n+1,isRow,isCol,isSquare9)) return true;isRow[i][num]=isCol[j][num]=isSquare9[k][num]=false;
​}board[i][j] = '.';return false;}
}

法三:极致优化版

这个方法比较难理解,时间充裕的同学可以尝试尝试,来自leetcode题解,代码及注释如下:

class Solution {// 二进制中1表示 对应位置已经有值了private int[] rows = new int[9];private int[] cols = new int[9];private int[][] cells = new int[3][3];public void solveSudoku(char[][] board) {int cnt = 0;for(int i=0; i<board.length; i++){for(int j=0; j<board[i].length; j++){char c = board[i][j];if(c == '.'){cnt++;}else{int n = c - '1';// rows[i] |= (1 << n);// cols[j] |= (1 << n);// cells[i/3][j/3] |= (1 << n);fillNumber(i, j, n, true);}}}backtrace(board, cnt);}
​private boolean backtrace(char[][] board, int cnt){if(cnt == 0){return true;}// 获取当前 候选项最少(即限制最多)的格子下标int[] pos = getMinOkMaskCountPos(board);int x = pos[0], y = pos[1];// okMask 值为1的 位 表示 对应的数字 当前可以填入int okMask = getOkMask(x, y);
​for(char c='1'; c<='9'; c++){int index = c - '1';if(testMask(okMask, index)){fillNumber(x, y, index, true);board[x][y] = c;if(backtrace(board, cnt-1)) return true; // 题目假定唯一解board[x][y] = '.';fillNumber(x, y, index, false); }}
​return false;}
​// n 0..8private void fillNumber(int x, int y, int n, boolean fill){// 因为回溯先选择后撤销,所以fill先true后false, false时对应位置一定是1,所以异或可行// rows[x] = fill ? rows[x] | (1<<n) : rows[x] ^ (1<<n);// cols[y] = fill ? cols[y] | (1<<n) : cols[y] ^ (1<<n);// cells[x/3][y/3] = fill ? cells[x/3][y/3] | (1<<n) : cells[x/3][y/3] ^ (1<<n);
​// ture set 1, false set 0rows[x] = fill ? rows[x] | (1<<n) : rows[x] & ~(1<<n);cols[y] = fill ? cols[y] | (1<<n) : cols[y] & ~(1<<n);cells[x/3][y/3] = fill ? cells[x/3][y/3] | (1<<n) : cells[x/3][y/3] & ~(1<<n);}
​private int getOkMask(int x, int y){return ~(rows[x] | cols[y] | cells[x/3][y/3]);}
​// mask 二进制 低9位 中 1的个数private int getOneCountInMask(int mask){int res = 0;for(int i=0; i<9; i++){int test = 1 << i;if((mask & test) != 0){res++;}}return res;}
​// mask 二进制 低index位 是否为 1private boolean testMask(int mask, int index){return (mask & (1 << index)) != 0;}
​// 获取候选项最少的位置private int[] getMinOkMaskCountPos(char[][] board){int[] res = new int[2];int min = 10;for(int i=0; i<board.length; i++){for(int j=0; j<board[i].length; j++){if(board[i][j] == '.'){int okMask = getOkMask(i, j);int count = getOneCountInMask(okMask);if(count < min){min = count;res[0] = i;res[1] = j;}}}}return res;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/63412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Next.js系统性教学:深入理解部分预渲染与边缘计算

更多有关Next.js教程&#xff0c;请查阅&#xff1a; 【目录】Next.js 独立开发系列教程-CSDN博客 目录 更多有关Next.js教程&#xff0c;请查阅&#xff1a; 1. 部分预渲染&#xff08;Partial Prerendering&#xff09; 1.1 什么是部分预渲染&#xff1f; 1.1.1 部分预渲…

Ubuntu 20.04安装rsync 3.2.7

前言 Ubuntu 20.04的apt中不支持rsync 3.2.0的安装&#xff0c;因此需要手动编译安装&#xff0c;记录下过程 ~$ apt policy rsync rsync:已安装&#xff1a;(无)候选&#xff1a; 3.1.2-2.1ubuntu1.6版本列表&#xff1a;3.1.3-8ubuntu0.7 -1100 /var/lib/dpkg/status下载所…

UE5基本数据类型

bool: 表示布尔值&#xff0c;只有两个取值&#xff1a;true 或 false&#xff0c;用于表示逻辑条件。int8: 表示 8 位的有符号整数&#xff0c;范围是 −128−128 到 127127。uint8: 表示 8 位的无符号整数&#xff0c;范围是 00 到 255255。int16: 表示 16 位的有符号整数&am…

Linux中的rpm命令

rpm&#xff08;Red Hat Package Manager&#xff09;是一个用于管理基于 RPM 的 Linux 发行版&#xff08;如 Red Hat、CentOS、Fedora 等&#xff09;软件包的命令行工具。你可以使用 rpm 命令来查询、安装、卸载、升级和验证 RPM 软件包。 ### 常用的 rpm 命令&#xff1a;…

【C++游戏程序】easyX图形库还原游戏《贪吃蛇大作战》(三)

承接上一篇文章&#xff1a;【C游戏程序】easyX图形库还原游戏《贪吃蛇大作战》&#xff08;二&#xff09;&#xff0c;我们这次来补充一些游戏细节&#xff0c;以及增加吃食物加长角色长度等设定玩法&#xff0c;也是本游戏的最后一篇文章。 一.玩家边界检测 首先是用来检测…

linux的vdagent框架设计

1、vdagent Linux 的 spice 客户代理由两部分组成&#xff0c;一个系统范围的守护进程 spice-vdagentd 和一个 X11 会话代理 spice-vdagent&#xff0c;每个 X11 会话有一个。spice-vdagentd 通过 Sys-V initscript 或 systemd 单元启动。 如下图&#xff1a;spice-vdagent&a…

docker修改并迁移存储至数据盘

文章目录 前言一、操作步骤&#xff08;需要root权限&#xff09;1. 查看磁盘占用&#xff0c;查看当前docker目录占用的空间2. 查看正在运行的容器&#xff0c;并停止容器及服务3. 拷贝数据、修改配置&#xff08;关键步骤&#xff09;4. 加载配置&#xff0c;启动服务及容器 …

证明网络中的流形成一个凸集

证明网络中的流形成一个凸集 步骤1&#xff1a;定义和符号步骤2&#xff1a;线性组合步骤3&#xff1a;验证容量限制步骤4&#xff1a;验证流量守恒结论示例代码&#xff08;C语言&#xff09; 在网络流理论中&#xff0c;一个流 f f f 是定义在网络图的边集上的一种函数&…

vscode(一)安装(ubuntu20.04)

1、更新软件包列表 sudo apt update2、安装依赖包 sudo apt install software-properties-common apt-transport-https wget3、导入Microsoft GPG密钥 wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -4、向系统添加VSCode存储库 sudo…

阿里云轻量应用服务器开放端口,图文教程分享

阿里云轻量应用服务器如何开放端口&#xff1f;在轻量服务器管理控制台的防火墙中添加规则即可开通端口&#xff0c;开通80端口就填80&#xff0c;开通443就填443端口&#xff0c;开通3306端口就填3306。阿里云百科网aliyunbaike.com整理阿里云轻量应用服务器端口号开通图文教程…

自然三次样条插值推导笔记

问题情境 假设我们有一组数据点&#xff08;称为控制点&#xff09;&#xff1a; x 0 , x 1 , x 2 , … , x n x_0, x_1, x_2, \ldots, x_n x0​,x1​,x2​,…,xn​ 这些点是已知的&#xff0c;表示我们要拟合的曲线在等距离参数点&#xff08;比如参数取为0,1,2,…,n&#x…

如何在Ubuntu中利用repo和git地址下载获取imx6ull的BSP

01-设置git的用户名和邮箱 git config --global user.name "suwenhao" git config --global user.email "2487872782qq.com"这里不设置的话后面在第5步的repo配置中还是会要求输入&#xff0c;而且以后进行相关操作都要输入&#xff0c;不妨现在就进行配置…

C++设计模式之外观模式

动机 下图中左边方案的问题在于组件的客户和组件中各种复杂的子系统有了过多的耦合&#xff0c;随着外部客户程序和各子系统的演化&#xff0c;这种过多的耦合面临很多变化的挑战。 如何简化外部客户程序和系统间的交互接口&#xff1f;如何将外部客户程序的演化和内部子系统…

13.高级GLSL

高级GLSL 1.GLSL的内建变量 着色器都是最简化的&#xff0c;如果需要当前着色器以外地方的数据的话&#xff0c;我们必须要将数据传进来。我们已经学会使用顶点属性、uniform和采样器来完成这一任务了。然而&#xff0c;除此之外&#xff0c;GLSL还定义了另外几个以gl_为前缀…

【bug】python pandas KeyError: ‘index’

【bug】python pandas KeyError: index’ 环境 pandas 2.2.3问题详情 代码 import pandas as pd# 创建一个示例 DataFrame data {id: [1, 2, 3],name: [Alice, Bob, Charlie],age: [100, 200, 300] } df pd.DataFrame(data) # 这里的reset_index()用于将 Seri…

使用数据层进行数据生命周期管理

作者&#xff1a;来自 Elastic Stef Nestor Elasticsearch 7.10 使配置数据生命周期变得不再那么复杂。在这篇博文中&#xff0c;我将介绍一些变化、如何使用它们以及一些最佳实践。 数据生命周期可以包含很多阶段&#xff0c;因此我们将涉及&#xff1a; 将集群划分为层&…

Jenkins环境一站式教程:从安装到配置,打造高效CI/CD流水线环境-Ubuntu 22.04.5 环境离线安装配置 Jenkins 2.479.1

文章目录 Jenkins环境一站式教程&#xff1a;从安装到配置&#xff0c;打造高效CI/CD流水线环境-Ubuntu 22.04.5 环境离线安装配置 Jenkins 2.479.1一、环境准备1.1 机器规划1.2 环境配置1.2.1 设置主机名1.2.2 停止和禁用防火墙1.2.3 更新系统 二、安装配置Jenkins2.1 安装JDK…

R的中文文本处理包--tmcn

文章目录 介绍tmcn 和 jieba 的关系函数&#xff1a;catUTF8toUTF8实例 介绍 tmcn 包是 R 语言中的一个用于处理和分析中文文本的包&#xff0c;特别适用于中文文本的分词、词频统计和文本挖掘等任务。以下是 tmcn 包的基本用法&#xff0c;包括安装、常用函数和示例。 一个用…

64 基于32单片机的温湿度检测

所有仿真详情导航: PROTEUS专栏说明-CSDN博客 目录 一、主要功能 二、硬件资源 三、主程序编程 四、资源下载 一、主要功能 基于STM32F103C8T6单片机,采用DHT11检测温湿度,通过OLED屏幕显示,温度小于15,则继电器吸合驱动热风扇转动,高于20停止,湿度低于40%,则对应…

用Python绘制医学热图

在医学研究和临床实践中&#xff0c;数据的可视化是不可或缺的一部分。通过直观的数据展示&#xff0c;医学专业人员可以更好地理解各种疾病的治愈率、治疗效果以及医院之间的差异。今天&#xff0c;我们将介绍一种强大的数据可视化工具——热图&#xff08;Heatmap&#xff09…