刷题计划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,一经查实,立即删除!

相关文章

UE5基本数据类型

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

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 是定义在网络图的边集上的一种函数&…

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

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

如何在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_为前缀…

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

作者&#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…

用Python绘制医学热图

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

WPF 本地生成验证码

1、效果如下图&#xff0c;点击图片可更新验证码&#xff08;其实图片就是一个Button的背景图&#xff09;。 2、主要使通过用户控件创建&#xff0c;UCVerificationCode.xaml代码如下。 <UserControl x:Class"UC.UCVerificationCode"xmlns"http://schemas.…

彻底理解ThreadLocal的应用场景和底层实现

一.概念 定义&#xff1a; ThreadLocal 是 Java 中所提供的线程本地存储机制&#xff0c;可以利用该机制将数据缓存在某个线程内部&#xff0c;该线程可以在任意时刻、任意方法中获取缓存的数据。 其实是可以通过调用 Set() 方法往里面存入值&#xff0c;存入的值是每个线程互…

视频 的 音频通道提取 以及 视频转URL 的在线工具!

视频 的 音频通道提取 以及 视频转URL 的在线工具&#xff01; 工具地址: https://www.lingyuzhao.top/toolsPage/VideoTo.html 它提供了便捷的方法来处理视频文件&#xff0c;具体来说是帮助用户从视频中提取音频轨道&#xff0c;并将视频转换为可以通过网络访问的URL链接。无…

shell自动显示当前git的branch

效果简介&#xff1a; 1. 如果没在git仓库&#xff0c;显示无变化 2. 如果在git仓库&#xff0c;显示当前分支 实现方法&#xff1a; 在~/.bashrc 里添加&#xff1a; function git_branch { test -d .git && branch"git branch | grep "^\*" | sed…

pytorch生成对抗网络

# 生成对抗网络 import os import torch import torchvision import torch.nn as nn from torchvision import transforms from torchvision.utils import save_image # Device configuration device torch.device(cuda if torch.cuda.is_available() else cpu) # 超参数 late…

从GCC源码分析C语言编译原理——源码表层分析(脚本篇)

目录 一、目录结构 二、有意思的小功能 三、install脚本 脚本变量和设置 程序名称变量 模式和命令 参数解析 主要逻辑 四、主要功能脚本 ------------------------------------------------------------------------------------------------------------------------…

自定义指令,全局,局部,注册

让输入框自动获取焦点(每次刷新自动获取焦点&#xff09; <template><div><h3>自定义指令</h3><input ref"inp" type"text"></div> </template><script> export default {mounted(){this.$refs.inp.focus…

二、点亮希望之光:寄存器与库函数驱动 LED 灯

文章目录 一、寄存器1、存储器映射2、存储器映射表3、寄存器4、寄存器映射5、寄存器重映射6、总线基地址、外设基地址、外设寄存器地址7、操作寄存器&#xff08;以操作一个GPIO口为例&#xff09;1. 寄存器地址定义部分2. GPIOD_Configuration 函数部分3. main 函数部分 二、库…

梯度下降法求解局部最小值深入讨论以及 Python 实现

文章目录 0. 前期准备1. 局部最小值2. 例子3. 讨论3.1 增加迭代次数3.2 修改学习率 η \eta η3.3 修改初始值 x 0 x^0 x0 4. 总结参考 0. 前期准备 在开始讲梯度下降法求解函数的局部最小值之前&#xff0c; 你需要有梯度下降法求解函数的最小值的相关知识。 如果你还不是…