代码随想录第三十天 | 回溯拓展:深搜与回溯,记录映射关系(leetcode 332),二维数组单层递归(51:N皇后),二维递归(leetcode 37)

1、深搜与回溯,记录映射关系

1.1 leetcode 332:重新安排行程

第一遍代码
主要不完全知道 map / unordered_map 的用法,所以没想到这个可以使用 unordered_map 记录使用 map 自然排序加记录,而且以为回溯一定没有返回值,知道需要用返回值后返回值的使用逻辑也不清晰,最后ac

返回值为bool,保证第一次整遍所有机场就马上返回
注意:map循环每次值的变量类型pair<const string,>& 注意是const

注意当机票用完时需要跳过,不然会出现
[[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]
Answer:[“JFK”,“ATL”,“JFK”,“ATL”,“JFK”,“ATL”] 自己循环起来了,其实票用完了
Expected Answer:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]

注意第一遍完整就返回的有返回值的情况)的递归回溯函数的部分怎么写
这边一定是return false,因为如果出现某一子树全部没有满足条件的解的话,需要回溯之后 尝试同一树层上的另外一个子树
如:
Testcase:[[“JFK”,“KUL”],[“JFK”,“NRT”],[“NRT”,“JFK”]]
Answer:[“JFK”,“KUL”] 没有以KUL打头的票了,应该换成for循环的下一个,也就是[“JFK”,“NRT”]
Expected Answer:[“JFK”,“NRT”,“JFK”,“KUL”]

class Solution {
public:
    unordered_map<string, map<string, int>> tar;
    int num = 1;
    //返回值为bool,保证第一次整遍所有机场就马上返回
    bool backTracking(vector<string>& res) {
        if(res.size() == num) {
            return true;
        }
        for(pair<const string, int>& tmp : tar[res[res.size() - 1]]) {
            //注意:map循环的每次值的变量是pair<const string,>&
            if(tmp.second == 0) {
                continue;
            }
            /*
            注意当机票用完时需要跳过,不然会出现
            [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
            Answer:["JFK","ATL","JFK","ATL","JFK","ATL"] 自己循环起来了,其实票用完了
            Expected Answer:["JFK","ATL","JFK","SFO","ATL","SFO"]
            */
            res.push_back(tmp.first);
            tmp.second--;
            if(backTracking(res)) return true;
            //注意第一遍完整就返回的有返回值的情况的递归回溯函数的部分怎么写,加上最后的return
            res.pop_back();
            tmp.second++;
        }
        return false;
        /*
        这边一定是return false,因为如果出现在某一子树全部没有满足条件的解的话
        需要回溯试同一树层上的另外一个子树
        如:
        Testcase:[["JFK","KUL"],["JFK","NRT"],["NRT","JFK"]]
        Answer:["JFK","KUL"] 没有以KUL打头的票了,应该换成for循环的下一个,也就是["JFK","NRT"]
        Expected Answer:["JFK","NRT","JFK","KUL"]
        */
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for(vector<string> iter : tickets) {
            tar[iter[0]][iter[1]]++;
            num++;
        }
        vector<string> res;
        res.push_back("JFK");
        backTracking(res);
        return res;
    }
};

map可以 通过下标来访问元素:
在 C++11 及以上版本中,std::map 提供了 operator[] 运算符的重载,可以用于通过键访问元素
但是要注意,operator[] 不允许对 std::map 中不存在的键进行访问,因为它会在访问不存在的键时 插入一个具有默认值的元素。因此,如果你想要 确保访问的键存在,可以 先使用 find() 函数进行检查

如果不使用下标访问元素,你可以使用迭代器 或者 find函数来访问map中的元素
1)使用迭代器遍历元素

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;

    // 向 map 中插入一些键值对
    myMap.insert({1, "One"});
    myMap.insert({2, "Two"});
    myMap.insert({3, "Three"});

    // 使用迭代器遍历 map 中的元素
    std::cout << "Elements in the map: ";
    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        std::cout << "(" << it->first << ", " << it->second << ") ";
    }
    std::cout << std::endl;

    return 0;
}

2)使用 find 函数查找元素:

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;

    // 向 map 中插入一些键值对
    myMap.insert({1, "One"});
    myMap.insert({2, "Two"});
    myMap.insert({3, "Three"});

    // 使用 find 函数查找特定键的元素
    auto it = myMap.find(2);
    if (it != myMap.end()) {
        std::cout << "Element found: (" << it->first << ", " << it->second << ")" << std::endl;
    } else {
        std::cout << "Element not found." << std::endl;
    }

    return 0;
}

set不可以通过下标来访问元素
使用 find() 函数查找元素

auto found = mySet.find(3);
    if (found != mySet.end()) {
        std::cout << "Element 3 found in the set." << std::endl;
    } else {
        std::cout << "Element 3 not found in the set." << std::endl;
    }

for (pair<const string, int> &ti : tic[cur])引用必须加const,不加引用符可选是否加const
std::map 的键类型是 const 的,这是因为 std::map 中的键值对是按照键的顺序排序的,并且不允许修改键。因此,在使用 std::map 进行遍历时,键类型必须声明为 const,以确保不会修改键的值

for (pair<string, int> ti : tic[cur])如果不加引用符号 &,则需要根据需要考虑是否加 const。如果你想要对遍历的键值对进行修改,那么就不需要加 const。但是,如果你不打算修改遍历的键值对,最好加上 const 以确保不会意外修改它们
在每次迭代中,ti 是 pair<string, int> 类型的对象,是 tic[cur] 中的键值对的一个拷贝。因此,即使你修改了 ti 的值,也不会影响 tic[cur] 中的对应键值对

class Solution {
private:
    unordered_map<string, map<string, int>> tic;
    bool backTracking(vector<string> &path, int &num) {
        if (path.size() == num) {
            return true;
        }
        string cur = path.back();
        for (pair<const string, int> &ti : tic[cur]) { // 一定是引用遍历,用到引用就要加const
            if (ti.second == 0)
                continue;
            ti.second--;
            path.push_back(ti.first);
            if (backTracking(path, num)) return true;
            ti.second++;
            path.pop_back();
        }
        return false;
    }
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        int num = 1; // 一开始就有一个城市了
        for (vector<string> &ticket : tickets) {
            tic[ticket[0]][ticket[1]]++;
            num++;
        }
        cout << num << endl;
        vector<string> res;
        res.push_back("JFK");
        backTracking(res, num);
        return res;
    }
};

思路
图论中的深度优先搜索,这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢
拓展一下,原来回溯法还可以这么玩

这道题目有几个难点:
1、一个行程中,如果航班处理不好容易变成一个圈成为死循环
2、有多种解法字母序靠前排在前面,让很多同学望而退步,该如何记录映射关系
3、使用回溯法(也可以说深搜) 的话,那么终止条件是什么
4、搜索的过程中,如何遍历一个机场所对应的所有机场

1、如何理解死循环
对于死循环,我来举一个有重复机场的例子:
深搜与回溯中的死循环
为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环

处理方法记录每条航线机票数量用完为止,就不再放入结果集了

2、记录映射关系
有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢
个机场映射多个机场,机场之间要靠字母序排列
个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset

这样存放映射关系可以定义为 unordered_map<string, multiset<string>> targets 或者 unordered_map<string, map<string, int>> targets
含义如下:

unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets
unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets

这两个结构,我选择了后者,因为如果使用unordered_map<string, multiset<string>> targets 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效

为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环
搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets

遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了
如果**“航班次数”大于零**,说明目的地还可以飞,如果**“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作
相当于说
不删**,就做一个标记
本题以输入:[[“JFK”, “KUL”], [“JFK”, “NRT”], [“NRT”, “JFK”]为例,抽象为树形结构如下:
记录递归的过程的树形图
开始回溯三部曲讲解:
1、递归函数参数返回值
在讲解映射关系的时候,已经讲过了,使用unordered_map<string, map<string, int>> targets; 记录航班的映射关系,我定义为全局变量
当然把参数放进函数里传进去也是可以的,是尽量控制函数里参数的长度
参数里还需要ticketNum,表示有多少个航班终止条件会用上),当然也可以像第一遍代码那样使用全局变量

代码如下:

// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {

注意函数返回值我用的是bool
我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢?
因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线找到了这个叶子节点了直接返回
如图:
在回溯的递归函数使用返回值
本题的targets和result都需要初始化对信息进行记录,代码如下:

for (const vector<string>& vec : tickets) {
    targets[vec[0]][vec[1]]++; // 记录映射关系
}
result.push_back("JFK"); // 起始机场

2、递归终止条件
拿题目中的示例为例,输入: [[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]] ,这是有4个航班,那么只要找出一种行程行程里的机场个数是5就可以了
所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了

代码如下:

if (result.size() == ticketNum + 1) {
    return true;
}

已经看习惯回溯法代码的同学,到叶子节点了习惯性的想要收集结果,但发现并不需要,本题的result相当于 Leetcode 216 中的path,也就是本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素

3、单层搜索的逻辑
回溯的过程中,如何遍历一个机场所对应的所有机场呢?

这里刚刚说过,在选择映射函数的时候,不能选择unordered_map<string, multiset<string>> targets, 因为一旦有元素增删 multiset 的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了
可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素迭代器还不能失效
所以我选择了unordered_map<string, map<string, int>> targets 来做机场之间的映射
遍历过程如下:

for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
    if (target.second > 0 ) { // 记录到达机场是否飞过了
        result.push_back(target.first);
        target.second--;
        if (backtracking(ticketNum, result)) return true;
        result.pop_back();
        target.second++;
    }
}

可以看出 通过unordered_map<string, map<string, int>> targets里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素

完整C++代码如下:

class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
    if (result.size() == ticketNum + 1) {
        return true;
    }
    for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
        if (target.second > 0 ) { // 记录到达机场是否飞过了
            result.push_back(target.first);
            target.second--;
            if (backtracking(ticketNum, result)) return true;
            result.pop_back();
            target.second++;
        }
    }
    return false;
}
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        vector<string> result;
        for (const vector<string>& vec : tickets) {
            targets[vec[0]][vec[1]]++; // 记录映射关系
        }
        result.push_back("JFK"); // 起始机场
        backtracking(tickets.size(), result);
        return result;
    }
};

代码中

for (pair<const string, int>& target : targets[result[result.size() - 1]])

一定要加上引用即 & target,因为后面有对 target.second 做减减操作,如果没有引用,单纯复制这个结果就没记录下来,那最后的结果就不对了
加上引用之后,就必须在 string 前面加上 const,因为map中的key 是不可修改了,这就是语法规定

1.2 leetcode 332:总结

如果单纯的回溯搜索(深搜)并不难,难还难在容器的选择和使用上

本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归
如果最终代码,发现照着回溯法模板画的话好像也能画出来,但难就难如何知道可以使用回溯

2、二维数组单层递归

2.1 leetcode 51:N皇后

第一遍代码(错误
没有四个一组,全部分隔开来
同时报错
整上二维矩阵就不会递归了,还是用以前数组递归的思路

class Solution {
public:
    vector<string> path;
    vector<vector<string>> res;
    int nw;
    bool isRight(vector<string> path) {
        int i = path.size() / nw + 1;
        int j = path.size() - (i - 1) * nw;
        for(int ii = 0; ii < i-1; ii++) {//纵向
            if(path[ii*nw + j - 1] == "Q") {
                return false;
            }
        }
        for(int jj = 0; jj < j-1; jj++) {//横向
            if(path[(i - 1)*nw + jj] == "Q") {
                return false;
            }
        }
        int minele = j;
        if(i < j) minele = i;
        while(minele > 0) {//斜向
            if(path[(i - 1)*nw + j - 1] == "Q") {
                return false;
            }
            minele--;
            i--;
            j--;
        }
        return true;
    }
    void backTracking(int leftNum) {
        if(leftNum == 0) {
            res.push_back(path);
            return;
        }
        for(int i = 0; i < nw; i++) {
            path.push_back("Q");
            if(isRight(path) == true) {
                for(int ii = i+1; ii < nw; ii++) {
                    path.push_back(".");
                }
                backTracking(leftNum-1);
                continue;
            }
            path.pop_back();
            path.push_back(".");
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        nw = n;
        backTracking(n);
        return res;
    }
};

思路
对于二维矩阵的单层回溯

首先来看一下皇后们的约束条件
1、不能同行
2、不能同列
3、不能同斜线

确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树
下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
N皇后问题搜索过程
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度矩阵的宽就是树形结构中每一个节点的宽度
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置

回溯三部曲
1、递归函数参数
我依然是定义全局变量二维数组result来记录最终结果
参数n棋盘的大小,然后用row来记录当前遍历到棋盘的第几层

代码如下:

vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {

2、递归终止条件
在如下树形结构中:
N皇后问题的递归过程 以三维矩阵为例
可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了,第一遍代码中的leftNum就相当于 n-row

代码如下:

if (row == n) {
    result.push_back(chessboard);
    return;
}

3、单层搜索的逻辑
递归深度就是row 控制棋盘的行每一层里for循环col 控制棋盘的列一行一列确定放置皇后的位置
每次都是要从新的一行的起始位置开始搜,所以都是从0开始

初始都是".",所以不需要像第一遍代码一样确定了Q的位置手动去放".",但是这样要多记录一个行数参数,不然不知道放到哪了

代码如下:

for (int col = 0; col < n; col++) {
    if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
        chessboard[row][col] = 'Q'; // 放置皇后
        backtracking(n, row + 1, chessboard);
        chessboard[row][col] = '.'; // 回溯,撤销皇后
    }
}

验证棋盘是否合法

按照如下标准去重
1、不能同行同层递归天然不可能出现同行的情况
2、不能同列
3、不能同斜线45度和135度角,注意有两个角度,第一遍代码只考虑了45度角,而且主要是要考虑 目标行列加减相同值的位置 而非 行列相同的位置

代码如下:

bool isValid(int row, int col, vector<string>& chessboard, int n) {
    // 检查列
    for (int i = 0; i < row; 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循环(也就是同一行)里的一个元素,所以不用去重
根据思路修改第一遍代码,ac了

for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
//第二个初始化的j不需要写int,因为前面是,
vector<string> tmp(nw, string(nw, '.'));//注意初始化的写法
class Solution {
public:
    vector<string> path;
    vector<vector<string>> res;
    int nw;
    bool isRight(vector<string>& square, int row, int col) {
        for(int i = 0; i < row; i++) {//纵向
            if(square[i][col] == 'Q') {
                return false;
            }
        }
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        //第二个初始化的j不需要写int,因为前面是,
            if(square[i][j] == 'Q') {
                return false;
            }
        }//45度角
        for(int i = row - 1, j = col + 1; i >= 0 && j < nw; i--, j++) {
            if(square[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
    void backTracking(int leftNum) {
        if(leftNum == 0) {
            res.push_back(path);
            return;
        }
        for(int i = 0; i < nw; i++) {
            path[nw - leftNum][i] = 'Q';
            if(isRight(path, nw - leftNum, i) == true) {
                backTracking(leftNum - 1);
            }
            path[nw - leftNum][i] = '.';
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        nw = n;
        vector<string> tmp(nw, string(nw, '.'));//注意初始化的写法
        path = tmp;
        backTracking(n);
        return res;
    }
};

代码随想录完整代码

class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
    if (row == n) {
        result.push_back(chessboard);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
            chessboard[row][col] = 'Q'; // 放置皇后
            backtracking(n, row + 1, chessboard);
            chessboard[row][col] = '.'; // 回溯,撤销皇后
        }
    }
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
    // 检查列
    for (int i = 0; i < row; i++) { // 这是一个剪枝
        if (chessboard[i][col] == 'Q') {
            return false;
        }
    }
    // 检查 45度角是否有皇后
    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {// 第二个j前面不用加int
        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:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

时间复杂度: O(n!)
空间复杂度: O(n)

2.1 leetcode 51:总结

回溯法不限于一维数组
棋盘的宽度就是for循环的长度递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了

3、二维递归

3.1 leetcode 37:解数独

第一遍代码,跟上一题leetcode 51一样思路按行进行一层一层递归不行,因为每一行上每个空白列都需要进行不断的递归
而且他每一个位置上放的是字符,不是数字
错误代码

class Solution {
public:
    //沿用上一题leetcode 51 N皇后的思路,唯一不同的就是判断
    bool isRight(vector<vector<char>>& board, int row, int col) {
        int i = row / 3;//计算其在小块的方位
        int j = col / 3;
        vector<bool> is9(9, false);//记录1-9是否只出现了一次
        //判断1-9是否在每一个以粗实线分隔的 3x3 宫内只能出现一次
        for(int ii = i*3; ii < i*3 + 3; ii++) {
            for(int jj = j*3; jj < j*3 + 3; jj++) {
                if(is9[board[ii][jj] - 1] == true) {
                    return false;
                }
                else {
                    is9[board[ii][jj] - 1] = true;
                }
            }
        }
        //数字 1-9 在每一行只能出现一次不需要判断,一层上的递归+回溯就可以保证这一点
        //判断数字 1-9 在每一列是否只出现一次
        vector<bool> is9(9, false);
        for(int i = row - 1; i >= 0; i--) {
            if(is9[board[i][col] - 1] == true) {
                return false;
            }
            else {
                is9[board[i][col] - 1] = true;
            }
        }
        return true;
    }
    bool backTracking(vector<vector<char>>& board, int row) {
        if(row == 9) {
            return true;
        }
        for(int i = 0; i < 9; i++) {
            if(board[row][i] != '.') {//确定目标位置
                continue;
            }
            for(int j = 1; j <= 9; j++) {
                //确定填入目标数字,发现一行要填多个也要递归,按行递归不行了
                board[row][i] = j;
                if(isRight(board, row, i)) {
                    continue;
                }
                else {
                    board[row][i] = j;
                }
            }
        }
    }
    void solveSudoku(vector<vector<char>>& board) {
        //已有的在board里面的数字不能往里面填了
        
    }
};

思路
这道题是二维递归,之前的leetcode 51 N皇后是在一个二维矩阵上进行的一维递归
大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:77.组合(组合问题),131.分割回文串(分割问题),78.子集(子集问题),46.全排列(排列问题),以及51.N皇后(N皇后问题),其实这些题目都是一维递归
leetcode 51 N皇后问题 是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行递归来遍历列,然后一行一列确定皇后的唯一位置

本题就不一样了,本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深
因为这个树形结构太大了,我抽取一部分,如图所示:
解数独问题 二维递归 的树形结构
回溯三部曲
1、递归函数返回值以及参数
递归函数的返回值需要是bool类型
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,同leetcode 51

参数不需要记录行号了,因为不是按行遍历了,整体遍历不需要参数

代码如下:

bool backtracking(vector<vector<char>>& board)

2、递归终止条件
本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回(for完自然结束了)

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

那么有没有永远填不满的情况呢
这个问题我在递归单层搜索逻辑里再来讲

3、递归单层搜索逻辑二维递归,以及碰到正确的就马上返回逻辑
解数独问题 二维递归 的树形结构
在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列一行一列确定下来之后,递归遍历这个位置放9个数字的可能性

对第一遍代码修改后ac
判断数字 1-9 在每一列是否只出现一次,每次都需要整列判断,即for循环需要从0-8
相同的变量不能重复定义两次
别忘了排除’.'的情况
回溯中的return false:这里要好好理解,到这了说明这个位置1-9没一个成返回false让上一层重新换数字

if (backtracking(board)) return true; 如果找到合适一组立刻返回,同时对于一个元素只检查当前所在方块//class Solution {
public:
    bool isRight(vector<vector<char>>& board, int row, int col) {
        int i = row / 3;//计算其在小块的方位,这样保证3*i,3*j在小方块左上角
        int j = col / 3;
        vector<bool> is9(9, false);//记录1-9是否只出现了一次
        //判断1-9是否在每一个以粗实线分隔的 3x3 宫内只能出现一次
        for(int ii = i*3; ii < i*3 + 3; ii++) {
            for(int jj = j*3; jj < j*3 + 3; jj++) {
                if(board[ii][jj] == '.') continue;
                if(is9[board[ii][jj] - '1'] == true) {
                    return false;
                }
                else {
                    is9[board[ii][jj] - '1'] = true;
                }
            }
        }
        //判断数字 1-9 在每一列是否只出现一次,每次都需要整列判断,即for循环需要从0-8
        //相同的变量不能重复定义两次,别忘了排除'.'的情况
        vector<bool> is92(9, false);
        for(int i = 0; i < 9; i++) { // 整列判断(0-9不是0-col)
            if(board[i][col] == '.') continue;
            if(is92[board[i][col] - '1'] == true) {
                return false;
            }
            else {
                is92[board[i][col] - '1'] = true;
            }
        }
        //判断行
        vector<bool> is93(9, false);
        for(int i = 0; i < 9; i++) { // 整行判断(0-9不是0-row)
            if(board[row][i] == '.') continue;
            if(is93[board[row][i] - '1'] == true) {
                return false;
            }
            else {
                is93[board[row][i] - '1'] = true;
            }
        }
        return true;
    }

    bool backTracking(vector<vector<char>>& board) {
        for(int i = 0; i < 9; i++) {
            for(int j = 0; j < 9; j++) {//确定填入位置
                if(board[i][j] != '.') {
                    continue;
                }
                for(char c = '1'; c <= '9'; c++) {//确定填入数字
                    board[i][j] = c;
                    if(isRight(board, i, j)) {
                        if(backTracking(board)) return true; // 碰到一组符合要求就返回
                    }
                    board[i][j] = '.';//到9不符合就填'.'
                }
                return false;
                //这里要好好理解,到这了说明这个位置1-9没一个成,返回false让上一层重新换数字,千万别漏了,注意位置在if内
            }
        }
        return true;// 一定要有返回值
    }

    void solveSudoku(vector<vector<char>>& board) {
        //已有的在board里面的数字不能往里面填了
        backTracking(board);
    }
};

代码随想录整体代码

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] == '.') {
                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;
        }
    }
    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;
}
public:
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

3.2 leetcode 37:总结

解数独可以说是非常难的题目了,如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃
所以我在开篇就提到了二维递归,这也是我(Carl)自创词汇,希望可以帮助大家理解解数独的搜索过程
一波分析之后,再看代码会发现其实也不难,唯一难点就是理解二维递归的思维逻辑

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值