主要思路
(1)参数和终止条件
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
定义了一个用于存储机场之间映射关系的数据结构 targets,以及一个递归函数 backtracking 用来实现深度优先遍历。
具体来说,targets 是一个 unordered_map,其中键为出发机场,值为一个 map。该 map 的键为到达机场,值为这两个机场之间对应的航班次数。这样构造的的是每个出发机场到目的机场的航班次数的映射,首先从一个机场可以到不止一个机场,其次两个相同机场之间不止一个航班
而 backtracking 函数用于实现深度优先遍历。该函数接受两个参数,一个是已经选择的机票数目 ticketNum,另一个是当前已选择的机场序列 result。该函数会根据已经选择的机票和当前所在的机场,查找可以前往的下一个机场,然后进行递归。当所有机票都被使用时,函数返回 true,并返回结果序列;否则,函数返回 false。这个函数的目的是求出满足题目要求的最小行程组合,即所有机票都必须用且只能用一次,并且按照字典序排序返回最小的行程组合。
初始化
for (const vector<string>& vec : tickets) { //遍历题目所给的二维tickers字符串
cout << vec[0] << " " << vec[1] << endl; //vec[0]表示所取字符串中起点,vec[1]表示终点
cout << targets[ vec[0] ] [ vec[1] ] << endl; //targets[ vec[0] ] [ vec[1] ]表示targets中以[ vec[0] ]为起点,[ vec[1] ]为终点的值
cout << "---------------------------------------" << endl;
targets[ vec[0] ] [ vec[1] ]++; // 记录映射关系
}
首先,使用一个范围 for 循环遍历输入的机票列表 tickets,每次循环获取一个机票 vec,类型为 vector<string>。
然后,将 vec[0](即机票的出发机场)作为键,vec[1](即机票的目的地机场)作为值,将它们的映射关系存储到一个二维 vector targets 中。这里使用了 C++ 中的 unordered_map 来实现,其语法为 targets[ vec[0] ] [ vec[1] ]++,表示将 vec[0] 和 vec[1] 映射关系的计数器加一,用来记录这两个机场之间有多少个航班。
最后,将起始机场 "JFK" 加入结果数组 result 中,作为遍历的起点。
注意函数返回值我用的是bool!因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图:
(2)终止条件:
回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么就找到了一个行程,把所有航班串在一起了。
(3)单层递归逻辑
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++;
}
}
【1】
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
// ...
}
使用范围 for 循环遍历了一个 map 容器 targets 中的一个子 map,
在 targets[result[result.size() - 1]]
中,result
表示当前已经遍历过的机场序列,result.size() - 1
表示当前到达的机场,result[result.size() - 1]
即为当前到达机场的名称,targets[result[result.size() - 1]]
表示以当前到达机场为起点的所有映射关系,其中键表示到达机场,值表示对应的航班数量。
易错点:
(1)容易陷入死循环:
出发机场和到达机场也会重复的,很可能就一直在JFK与NRT里一直兜圈
代码实现:
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]]) {
//result的声明是vector<string>,用于存储飞行路线
//result[result.size() - 1]表示的是以result中最后一个元素为起点,遍历该元素对应的所有可能路线
//pair<const string, int>& target表示终点机场和机票数量的map
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;
result.push_back("JFK"); // 起始机场
for (const vector<string>& vec : tickets) { //遍历题目所给的二维tickers字符串
targets[ vec[0] ] [ vec[1] ]++; // 记录映射关系
}
backtracking(tickets.size(), result);
return result;
}
};
补充
(1)C++中类循环
语法形式
for (element : container) {
// 迭代操作
}
type 是容器中元素的类型,var 是循环中变量的名称,container 是容器本身。该循环语句用于遍历容器中的所有元素,并对每个元素执行指定的语句。
其中 container
可以是任何可迭代对象,例如 std::vector
、std::map
、std::set
、C数组等等,而 element
则是 container
中的元素。
循环过程中,每一次迭代都会把 container
中的元素赋值给 element
,然后执行循环体中的语句。对于 std::vector
、std::array
、C数组等顺序容器,迭代顺序是按照容器中元素的顺序;对于 std::map
、std::set
等关联容器,则是按照元素的 key 值升序遍历。
例如,下面的代码使用类循环遍历一个 std::vector<int>
容器:
std::vector<int> nums = {1, 2, 3, 4, 5};
for (int num : nums) {
std::cout << num << " ";
}
// 输出:1 2 3 4 5
如果需要修改 container
中的元素,必须使用引用类型的 element
,例如:
std::vector<int> nums = {1, 2, 3, 4, 5};
for (int& num : nums) { // 使用引用类型
num *= 2; // 修改元素
}
for (int num : nums) {
std::cout << num << " ";
}
// 输出:2 4 6 8 10
如果要遍历unordered_map<string, map<string, int>> targets;
for (auto& [from, to_map] : targets) {
cout << "出发机场" << from << endl;
for (auto& [to, count] : to_map) {
cout << "终点机场" << to << '\t' << "机票数量" << count << endl;
}
cout << "----------------------------------" << endl;
}
在这个代码中,外部的for
循环遍历targets
的键值对,其中from
为出发机场的名字,to_map
为以该出发机场为键的map<string, int>
类型,其储存了到达机场和航班次数的键值对。内部的for
循环遍历每个to_map
中的键值对,其中to
为到达机场的名字,count
为该航线的航班次数。
上面中规中矩写法
for (const std::pair<std::string, std::map<std::string, int>>& target1 : targets) {
const std::string& departure = target1.first;
const std::map<std::string, int>& arrivalMap = target1.second;
for (const std::pair<std::string, int>& target2 : arrivalMap) {
const std::string& arrival = target2.first;
const int& flightCount = target2.second;
// 处理每个起点、终点及对应的航班次数
}
}
主要思路:
(1)皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
(2)二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
(矩阵的高是指矩阵中行的数量,通常用字母m表示;矩阵的宽是指矩阵中列的数量,通常用字母n表示)
代码实现:
class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
// for(int i = 0; i < chessboard.size(); i++) {
// cout << chessboard[i] << endl;
// }
// cout << "--------------------------------" << endl;
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--) {
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();
vector<string> chessboard(n, string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
};
补充
(1)vector<string> chessboard(n, string(n, '.'))
这行代码创建了一个大小为n x n的字符串矩阵,用于表示N皇后问题的棋盘。具体来说,它使用了vector<string>的构造函数,该构造函数接受两个参数,分别为vector的大小和初始值。
具体而言,该构造函数的第一个参数n表示vector中元素的个数,第二个参数string(n, '.')表示初始化每个元素为一个大小为n的字符串,其中每个字符都为'.',即棋盘上的初始状态为每个位置都没有皇后。
因此,这行代码的作用就是创建一个n x n的字符串矩阵,用于表示N皇后问题的棋盘,其中每个位置都没有皇后,即棋盘的初始状态。这个字符串矩阵被存储在一个vector<string>类型的变量chessboard中。
如果不是因为要读取每次棋盘大小n,chessboard其实可以像其他回溯题一样定为全局变量
力扣
视频讲解
易错点:
(1)终止条件:无,因为如果遍历完整个矩阵发现都已经填满,那自然会回溯返回true,如果有一个格子发现一个数字都不好填,那就会回溯返回false
(2)二维遍历整个矩阵,不像上面N皇后遍历行就行,因为上题遍历的一行里只会有一个皇后,本题则是要把矩阵填满
(3)每一个格子填上就可以return true,只有有一个填不上就return false,最后循环遍历矩阵结束都没有return false就return true
(4)判断一个位置应该放那个数字中最难就是九宫格里,关键是确定这是第几个九宫格,可以利用整除知识理解
代码实现:
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] == '.') { //如果这个位置为空(题目中用"."表示),就开始遍历1~9数字
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);
}
};
算法训练营额外收获
(1)什么时候加 std
std表示命名空间,头文件有using namespace std;就不用加