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的时候,不能删除元素,一旦删除元素,迭代器就失效了
class Solution {
public:
map<string, multiset<string>> myMap;
vector<string> path;
int num;
bool backTracking() {
if (path.size() == num) {
return true;
}
string start = path.back();
auto it = myMap.find(start);
if (it != myMap.end()) {
for (int i = 0; i < myMap[start].size(); i++) {
auto pos = next(myMap[start].begin(), i);
myMap[start].erase(pos);
if (myMap[start].empty()) {
myMap.erase(start);
}
path.push_back(*pos); // 删除了之后迭代器就失效了
if (backTracking()) return true;
myMap[start].insert(*pos);
path.pop_back();
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
int num = 1;
for (vector<string> &ticket : tickets) {
myMap[ticket[0]].insert(ticket[1]);
num++;
}
path.push_back("JFK");
backTracking();
return path;
}
};
std::multiset 是 C++ 标准库中的一个有序容器,但它不支持 pop_back() / pop() 操作。原因是 std::multiset 是基于 红黑树实现的有序集合,并不是像 std::vector 或 std::deque 那样的连续容器。因此,std::multiset 也不支持类似 back() 直接访问最后一个元素的操作
std::map 在 C++ 中没有内置的 pop 操作。与 std::multiset 一样,std::map 也是基于平衡树(如红黑树)实现的有序关联容器,它不支持直接的 pop_front 或 pop_back 方法
pop() 操作主要用于支持 栈(stack-like) 和 队列(queue-like) 行为的容器
容器适配器:std::stack、std::queue、std::priority_queue
双端队列:std::deque,提供 pop_front() 和 pop_back()
可以通过 std::prev(mset.end()) 获取最大元素的迭代器,并使用 erase 方法删除该元素,以实现类似 pop_back() 的效果
#include <set>
#include <iostream>
int main() {
std::multiset<int> mset = {1, 2, 3, 4, 5};
if (!mset.empty()) {
auto it = std::prev(mset.end()); // 获取指向最后一个元素的迭代器
mset.erase(it); // 删除最后一个元素
}
for (int num : mset) {
std::cout << num << " ";
}
return 0;
}
在 std::map<std::string, std::multiset<std::string>> 中,如果你删除了 multiset 中的所有元素,它并不会自动删除 map 中的对应键值对。你需要手动检查 multiset 是否为空,并在为空时手动从 map 中删除该键
#include <iostream>
#include <map>
#include <set>
#include <string>
int main() {
std::map<std::string, std::multiset<std::string>> myMap;
// 初始化 map
myMap["key1"].insert("value1");
myMap["key1"].insert("value2");
myMap["key2"].insert("value3");
// 删除 key1 对应的 multiset 中的所有元素
myMap["key1"].clear();
// 检查 key1 对应的 multiset 是否为空,并手动删除键
if (myMap["key1"].empty()) {
myMap.erase("key1");
}
// 打印 map 的内容
for (const auto& [key, mset] : myMap) {
std::cout << key << ": ";
for (const auto& value : mset) {
std::cout << value << " ";
}
std::cout << std::endl;
}
return 0;
}
如果你想把 multiset 的迭代器往前推进 i 位,可以使用 std::prev 函数。std::prev 可以从给定的迭代器向前移动指定的步数
std::prev(iterator, i);
// iterator 是你想要操作的迭代器。i 是你想向前移动的步数
如果你想将 std::multiset 的迭代器向后移动 i 位,可以使用 std::next 函数。std::next 和 std::prev 的用法类似,但 std::next 是向后移动迭代器
std::next(iterator, i);
// iterator 是你想要操作的迭代器。i 是你想向后移动的步数
为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环
当然,为了避免这种情况,可以使用以下两种方法:
1)使用 erase 并更新迭代器
在 C++ 中,当你想在遍历过程中删除元素,可以通过 erase 方法并更新迭代器来保证安全性
#include <set>
#include <iostream>
int main() {
std::multiset<int> mset = {1, 2, 3, 2, 4, 2};
for (auto it = mset.begin(); it != mset.end(); ) {
if (*it == 2) {
it = mset.erase(it); // 删除元素并返回下一个有效的迭代器
} else {
++it;
}
}
for (int num : mset) {
std::cout << num << " ";
}
return 0;
}
2) 使用复制数据结构
先复制 需要删除的元素,遍历完成后再统一删除
#include <set>
#include <vector>
#include <iostream>
int main() {
std::multiset<int> mset = {1, 2, 3, 2, 4, 2};
std::vector<int> to_erase;
for (auto it = mset.begin(); it != mset.end(); ++it) {
if (*it == 2) {
to_erase.push_back(*it);
}
}
for (int num : to_erase) {
mset.erase(mset.find(num));
}
for (int num : mset) {
std::cout << num << " ";
}
return 0;
}
使用 erase 方法传入一个值,它会删除 multiset 中所有与该值相等的元素
mset.erase(2) 会删除 mset 中所有等于 2 的元素
如果你只想删除某个具体的元素(而不是删除所有相同值的元素),可以使用传入迭代器的 erase 方法
#include <set>
#include <iostream>
int main() {
std::multiset<int> mset = {1, 2, 3, 2, 4, 2};
auto it = mset.find(2); // 找到第一个等于 2 的元素
if (it != mset.end()) {
mset.erase(it); // 仅删除一个等于 2 的元素
}
for (int num : mset) {
std::cout << num << " ";
}
return 0;
}
搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets
在遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了
如果**“航班次数”大于零**,说明目的地还可以飞,如果**“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作
相当于说不删**,就做一个标记
开始回溯三部曲讲解:
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 的棋盘,将搜索过程抽象为一棵树,如图:

从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了
回溯三部曲
1、递归函数参数
我依然是定义全局变量二维数组result来记录最终结果
参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了
代码如下:
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {
2、递归终止条件
在如下树形结构中:

可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了,第一遍代码中的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 {
public:
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 c = '1'; c <= '9'; c++) {
board[i][j] = c;
if (isTrue(board, i, j)) {
if (backTracking(board)) {
return true;
}
}
}
board[i][j] = '.';
// 要变回去,因为可能之后有不符合要求的情况,要不影响下一次尝试
return false;
}
}
}
return true;
}
bool isTrue(vector<vector<char>> &vec, int row, int col) {
char ele = vec[row][col];
// 纵向
for (int i = 0; i < vec.size(); i++) {
if (i == row) continue;
if (vec[i][col] == ele)
return false;
}
// 横向
for (int j = 0; j < vec[0].size(); j++) {
if (j == col) continue;
if (vec[row][j] == ele)
return false;
}
// 9宫格
int start_row = (row / 3) * 3;
int start_col = (col / 3) * 3;
for (int i = start_row; i < start_row + 3; i++) {
for (int j = start_col; j < start_col + 3; j++) {
if (i == row && j == col) continue;
if (vec[i][j] == ele)
return false;
}
}
return true;
}
void solveSudoku(vector<vector<char>>& 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)自创词汇,希望可以帮助大家理解解数独的搜索过程
一波分析之后,再看代码会发现其实也不难,唯一难点就是理解二维递归的思维逻辑
561

被折叠的 条评论
为什么被折叠?



