This commit is contained in:
youngyangyang04
2020-11-18 10:45:48 +08:00
parent ffa6b3d906
commit 19667d90bc
7 changed files with 218 additions and 63 deletions

View File

@ -1,51 +1,170 @@
# 题目地址
> 解数独,理解二维递归是关键
https://leetcode-cn.com/problems/sudoku-solver/
# 37. 解数独
题目地址https://leetcode-cn.com/problems/sudoku-solver/
编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 '.' 表示。
![解数独](https://img-blog.csdnimg.cn/202011171912586.png)
一个数独。
![解数独](https://img-blog.csdnimg.cn/20201117191340669.png)
答案被标成红色。
提示:
* 给定的数独序列只包含数字 1-9 和字符 '.' 。
* 你可以假设给定的数独只有唯一解。
* 给定数独永远是 9x9 形式的。
# 思路
棋盘搜索问题依然是要使用回溯法暴力搜索,只不过这次我们要做的是双层递归
棋盘搜索问题可以使用回溯法暴力搜索,只不过这次我们要做的是**二维递归**
怎么做双层递归呢?如果大家之前做过回溯类型的题目例如77.组合组合问题131.分割回文串分割问题78.子集子集问题46.全排列排列问题以及51.N皇后N皇后问题等等都会发现这些都是单层递归。
怎么做二维递归呢?
**如果大家以上这几道题目没有做过的话,不建议上来就做这道题哈!**
大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:[77.组合(组合问题)](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[131.分割回文串(分割问题)](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)[78.子集(子集问题)](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)[46.全排列(排列问题)](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw),以及[51.N皇后N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg),其实这些题目都是一维递归。
n皇后因为每一行每一列只放一个皇后只需要一层for循环遍历行数递归来处理每一行的一个皇后放在哪里就可以。
**如果以上这几道题目没有做过的话,不建议上来就做这道题哈!**
本题就不一样了本题中棋盘每一个位置都要放一个数字这个问题的树形结构要比N皇后更宽更深
[N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)是因为每一行每一列只放一个皇后只需要一层for循环遍历一行递归来来遍历列然后一行一列确定皇后的唯一位置
如图所示:
本题就不一样了,**本题中棋盘的每一个位置都要放一个数字并检查数字是否合法解数独的树形结构要比N皇后更宽更深**。
<img src='../pics/37.解数独.png' width=600> </img></div>
因为这个树形结构太大了,我抽取一部分,如图所示:
可以看出我们需要的是一个二维的递归也就是两个for循环递归遍历每一个棋盘的位置
![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png)
这道题目和之前递归的方式都不一样,这里相当于两层递归,之前的都是一层递归。
# C++代码
## 回溯三部曲
* 递归函数以及参数
**递归函数的返回值需要是bool类型为什么呢**
因为解数独找到一个符合的条件就在树的叶子节点上立刻就返回相当于找从根节点到叶子节点一条唯一路径所以需要使用bool返回值这一点在[回溯算法N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)中已经介绍过了,一样的道理。
代码如下:
```
class Solution {
private:
bool backtracking(vector<vector<char>>& board)
```
* 递归终止条件
本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
**不用终止条件会不会死循环?**
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
**那么有没有永远填不满的情况呢?**
这个问题我在递归单层搜索逻辑里在来讲!
* 递归单层搜索逻辑
![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png)
在树形图中可以看出我们需要的是一个二维的递归也就是两个for循环嵌套着递归
**一个for循环遍历棋盘的行一个for循环遍历棋盘的列一行一列确定下来之后递归遍历这个位置放9个数字的可能性**
代码如下:(**详细看注释**
```C++
bool backtracking(vector<vector<char>>& board) {
for (int i = 0; i < board.size(); i++) { // 遍历行
for (int i = 0; i < board.size(); i++) { // 遍历行
for (int j = 0; j < board[0].size(); j++) { // 遍历列
if (board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board)) {
board[i][j] = k;
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = '.';
board[i][j] = '.'; // 回溯撤销k
}
}
return false;
return false; // 9个数都试完了都不行那么就返回false
}
}
return true; // 遍历完没有返回false说明找到了合适棋盘位置了
}
```
**注意这里return false的地方这里放return false 是有讲究的**。
因为如果一行一列确定下来了这里尝试了9个数都不行说明这个棋盘找不到解决数独问题的解
那么会直接返回, **这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!**
## 判断棋盘是否合法
判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
代码如下:
```C++
bool isValid(int row, int col, char val, vector<vector<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++) { // 判断9方格里是否重复
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == val ) {
return false;
}
}
}
return true;
}
```
最后整体代码如下:
# C++代码
```C++
class Solution {
private:
bool backtracking(vector<vector<char>>& board) {
for (int i = 0; i < board.size(); i++) { // 遍历行
for (int j = 0; j < board[0].size(); j++) { // 遍历列
if (board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board)) {
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = '.'; // 回溯撤销k
}
}
return false; // 9个数都试完了都不行那么就返回false
}
}
return true; // 遍历完没有返回false说明找到了合适棋盘位置了
}
bool isValid(int row, int col, char val, vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) { // 判断行里是否重复
if (board[row][i] == val) {
return false;
@ -73,3 +192,18 @@ public:
}
};
```
# 总结
解数独可以说是非常难的题目了,如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃。
所以我在开篇就提到了**二维递归**,这也是我自创词汇,希望可以帮助大家理解解数独的搜索过程。
一波分析之后,在看代码会发现其实也不难,唯一难点就是理解**二维递归**的思维逻辑。
**这样,解数独这么难的问题,也被我们攻克了**
**恭喜一路上坚持打卡的录友们,回溯算法已经接近尾声了,接下来就是要一波总结了**
如果一直跟住「代码随想录」的节奏,你会发现自己进步飞快,从思维方式到刷题习惯,都会有质的飞跃,「代码随想录」绝对值得推荐给身边的同学朋友们!

View File

@ -36,7 +36,6 @@ n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并
class Solution {
private:
int count = 0;
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
count++;
@ -75,9 +74,7 @@ bool isValid(int row, int col, vector<string>& chessboard, int n) {
public:
int totalNQueens(int n) {
result.clear();
std::vector<std::string> chessboard(n, std::string(n, '.'));
vector<vector<string>> result;
backtracking(n, 0, chessboard);
return count;

View File

@ -1,20 +1,65 @@
# 思路
这道题目贪心不好讲解啊
## 方法一
如果 get总和大于cost总和那么一定是可以跑一圈的因为油是可以存储的。
本题贪心的思路,不是那么好想。
那么来看一下贪心主要贪在哪里
来看一下贪心主要贪在哪里:
* 如果gas的总和小于cost总和那么无论从哪里出发一定是跑不了一圈的
* remain[i] = gas[i]-cost[i]为一天剩下的油remain[i]i从0开始计算累加到最后一站如果累加没有出现负数说明从0出发油就没有断过那么0就是起点。
* 如果累加的最小值是负数就要从非0节点出发从后向前个节点能这个负数填平。
* 如果累加的最小值是负数就要从非0节点出发从后向前个节点能这个负数填平。
代码如下:
```
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int min = INT_MAX; // 从起点出发,油箱里的油量
for (int i = 0; i < gas.size(); i++) {
int remain = gas[i] - cost[i];
curSum += remain;
if (curSum < min) {
min = curSum;
}
}
if (curSum < 0) return -1; // 如果总油量-总消耗都小于零,一定是哪里作为起点都不行
if (min >= 0) return 0; // 从0的位置出发油箱里的油量没有出现负数说明从0触发可以跑一圈
// 否则就一定是从其他节点触发
// 从后向前遍历如果那个节点可以补上从0触发油箱出现负数的情况那么这个i就是起点
for (int i = gas.size() - 1; i >= 0; i--) {
int remain = gas[i] - cost[i];
min += remain;
if (min >= 0) {
return i;
}
}
return -1;
}
};
```
其实这份代码还是比较复杂的。
## 方法二
换一个思路,首先如果总油量减去总消耗大于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量remain[i]相加一定是大于零的。
每个加油站的剩余量remain[i]为gas[i] - cost[i]。
i从0开始累加remain[i]和记为curSum如果curSum小于零说明 [0, i]区间都不能作为起始位置起始位置从i+1算起。
如图:
<img src='../pics/134.加油站.png' width=600> </img></div>
那么为什么[ij] 区间和为负数已经起始位置就可以是j+1呢j+1后面就不会出现更大的负数
可以这么理解 j之前出现了多少负数j后面就会出现多少正数因为耗油总和是大于零的前提我们已经确定了一定可以跑完全程
代码如下:
这个方法太绝了
```
class Solution {
public:
@ -30,42 +75,11 @@ public:
curSum = 0;
}
}
if (totalSum < 0) return -1;
if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
return start;
}
};
```
这个方法太复杂了
```
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int remainSum = 0;
int min = INT_MAX; // 从起点出发,油箱里的油量
for (int i = 0; i < gas.size(); i++) {
int remain = gas[i] - cost[i];
remainSum += remain;
if (remainSum < min) {
min = remainSum;
}
}
if (remainSum < 0) return -1; // 如果总油量-总消耗都小于零,一定是哪里作为起点都不行
if (min >= 0) return 0; // 从0的位置出发油箱里的油量没有出现负数说明从0触发可以跑一圈
// 否则就一定是从其他节点触发
// 从后向前遍历如果那个节点可以补上从0触发油箱出现负数的情况那么这个i就是起点
for (int i = gas.size() - 1; i >= 0; i--) {
int remain = gas[i] - cost[i];
min += remain;
if (min >= 0) {
return i;
}
}
return -1;
}
};
```

View File

@ -126,6 +126,14 @@
* [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)
* [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw)
* [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)
* [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)
* [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)
* [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA)
* [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)
* [本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag)
* [视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg)
* [回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA)
* [回溯算法N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)
(持续更新.....