今天的题都很抽象,主要理解理解思路
332.重新安排行程
本题过于抽象,关键是捋清树形结构
大致看一看:本题还是在一路上选择飞往哪里,是在收集路径,因此选用记录边的回溯代码模板
然后本题只需要找到一个结果就能停止回溯搜索了。在Day18的112.路径总和中,我们介绍过:能够通过回溯函数的返回值标记“搜索到结果了”,从而使整体回溯过程在找到一个结果后就能停止继续搜索,不断向上返回
本题的一大难点:需要用一种方法知道在当前节点有哪些路径可选择? 需要根据机票信息,记录下从出发机场能到达的所有机场,以及从出发机场到各机场的航班次数
用数据结构unordered_map<出发机场, map<到达机场, 航班次数> >来存储上述信息
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
根据机票对该数据结构进行初始化
for (const vector<string>& vec : tickets) {
targets[vec[0]][vec[1]]++; // 记录映射关系
}
有了这些准备工作,就可以开始写代码了
class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
vector<string> path; // 用于记录一路上到达的城市
int ticketNum;
bool backtracking() {
if (path.size() == ticketNum + 1) { // 终止条件,告诉上一层“找到目标结果了”
return true;
}
const string & now = path.back(); // 当前所在机场为path的最后一个记录
for (pair<const string, int>& target : targets[now]) {
if (target.second > 0 ) { // 说明还有前往target.first的机票(还有剩余航班),这里我们看出了,targets就是在控制到达某个节点的可选择路径
path.push_back(target.first); // 记录路径
target.second--; // 标记使用了该机票
bool flag = backtracking(); // 前往这条路径,并返回“在树上按这条路走下去是否找到目标结果了”
if (flag) return true; // 如果已经找到了,就直接告诉再上层找到目标结果了
path.pop_back(); // 回溯,撤销记录
target.second++; // 撤销该机票的使用标记
}
}
return false; // 如果能够执行到这里,说明上述for循环中没有retuan true,进一步表明走到树的这个节点后,没有一条可行方案能够找到结果,需要向上层返回false
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
for (const vector<string>& vec : tickets) {
targets[vec[0]][vec[1]]++; // 记录映射关系,将所有机票信息整合进我们的数据结构
}
path.push_back("JFK"); // 起始机场
ticketNum = tickets.size();
backtracking();
return path; // 这里的path就是result
}
};
51.N皇后
本题也很抽象。看看树形结构吧
本题需要通过“放置位置的合法性”确定在某个节点时,有哪些可选择路径
本题回溯函数传入参数rowIndex来标记“当前走到了哪一行的节点”
chessboard记录的是合法的棋盘,因为我们是在做记录之前判断合法性的,就能够保证chessboard一直是合法的
class Solution {
private:
vector<string> chessboard;
vector<vector<string> > result;
public:
vector<vector<string>> solveNQueens(int n) {
chessboard = vector<string>(n, string(n, '.'));
backtracking(0);
return result;
}
void backtracking(int rowIndex) {
if (rowIndex > chessboard.size() - 1) { // 终止条件,rowIndex超过了最大行索引,则记录结果并返回
result.push_back(chessboard);
return;
}
for (int colIndex = 0; colIndex < chessboard.size(); ++colIndex) { // 开始选择路径
if (isValid(rowIndex, colIndex)) { // 如果在chessboard[rowIndex][colIndex]放置是合法的
chessboard[rowIndex][colIndex] = 'Q'; // 放置
backtracking(rowIndex + 1); // 前往下一行进行放置
chessboard[rowIndex][colIndex] = '.'; // 撤销放置
} else
continue;
}
return;
}
bool isValid(int rowIndex, int colIndex) { // 检查在chessboard[rowIndex][colIndex]放置是否合法
// 检查列是否有皇后
for (int i = 0; i < rowIndex; i++) {
if (chessboard[i][colIndex] == 'Q')
return false;
}
// 检查行是否有皇后(不需要检查行,因为在backtracking的逻辑中我们能够保证每行只有一个皇后)
// 检查45度角是否有皇后
for (int i = rowIndex - 1, j = colIndex - 1; i >= 0 && j >= 0; --i, --j) {
if (chessboard[i][j] == 'Q')
return false;
}
// 检查135度角是否有皇后
for (int i = rowIndex - 1, j = colIndex + 1; i >= 0 && j < chessboard.size(); --i, ++j) {
if (chessboard[i][j] == 'Q')
return false;
}
return true;
}
};
37.解数独
树形结构如下
本题仅需要搜索到一个结果即可,故回溯函数可以有返回值标记是否在树枝上找到结果
本题大致思路:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性
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,说明找到了合适棋盘位置了
}
整体代码
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);
}
};
回顾总结
回溯章节结束。我们在回溯章节的题目中主要的关注点:
怎么抽象出树形结构?
怎么确定在当前节点有哪些路径可去?
这两个是重点,我们的去重等操作目的也是为了排除一些不可去的边从而找到“找到哪些路径可去”