算法学习记录~2023.X.XX~章节DayX~题目号.题目标题 & 题目号.题目标题
前言
第一次一天的所有题目都是hard…
好在终于是过完一遍回溯了
332.重新安排行程
题目链接
思路1:不使用map和set
关键问题以及对应解法:
- 去除环路 --> 使用used数组记录机票的使用情况,使用过就不能再使用
- 字典序返回 --> 预先按照每张票的终点进行机票排序,这样回溯算法找到的第一个一定是字典排序最小的
具体的实现结合下面代码的注释进行理解
代码
class Solution {
public:
vector<string> path;
vector<vector<string>> result;
bool used[301] = {false}; //题目有说机票数量最多为300
bool find = false; //用于找到一条合规路径就停止继续加入到result
static bool cmp (const vector<string>& s1, const vector<string>& s2){ //比较函数,用于排字典序
return s1[1] < s2[1];
}
void backtracking (vector<vector<string>>& tickets, string outset){
if (find == true) //只要找到一个就返回即可,result只保存第一条路径
return ;
if (path.size() == tickets.size() + 1){ //全都用完则就是目标结果
find = true;
result.push_back(path);
return;
}
for (int i = 0; i < tickets.size(); i++){
if (tickets[i][0] == outset && used[i] == false){ //遍历到的机票的出发地符合起始点且没记用过则可以进行处理
used[i] = true;
path.push_back(tickets[i][1]); //机票的目的地即为下一趟路程的起始点
backtracking(tickets, tickets[i][1]);
used[i] = false;
path.pop_back();
}
}
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
sort(tickets.begin(), tickets.end(), cmp); //先按照目的地字典排序
path.push_back("JFK"); //第一个起点
backtracking(tickets, "JFK");
return result[0];
}
};
思路2:使用map
这个解法的思路以及中间好几条代码并没有完全理解,后续还需要再看
记录映射关系:
一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场。
可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。
unordered_map<string, map<string, int>> targets
unordered_map<出发机场, map<到达机场, 航班次数>> targets
代码
class Solution {
public:
//unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking (int ticketNum, vector<string>& result){ //使用bool因为找到一条符合的路径就立即返回
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;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> result;
for (const vector<string>& vec : tickets){ //**不太懂
targets[vec[0]][vec[1]]++; //记录映射关系
//targets[vec[0]]访问了map<string,int>(目的地map),然后[vec[1]]访问了map<string,int>的value(航程中目的地经过次数)
}
result.push_back("JFK");
backtracking(tickets.size(), result);
return result;
}
};
总结
本题如果用map和set的解法目前自己来看还是太吃力了,无论是思路还是基础语法都非常模糊,花了很久其实还并没有搞懂。
再刷时候可以考虑js的其他思路和解法
51. N皇后
题目链接
思路
首先是约束条件:
- 不能同行
- 不能同列
- 不能同斜线
搜索皇后的位置,可以抽象为一棵树,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
参数需要总体的n,当前遍历到第几层的row,以及棋盘chessboard。
终止条件就是搜索到了叶子节点,也就是搜到了最后一层。
单层逻辑中,就是对于每一层都从左到右依次检查该位置合法性,如果满足就继续向下一层递归,否则就继续向本层的右或进入下一层。
验证合法性分别检查列,45度角和135度角即可,只向当前层的上层进行判断。对于本层,因为是一个个循环,所以不需要检查本行。
代码
class Solution {
public:
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 column = 0; column < n; column++){ //搜索第row行,column为每一列
if (isValid(row, column, chessboard, n)){ //合法则可以放置皇后
chessboard[row][column] = 'Q';
backtracking(n, row + 1, chessboard); //继续向下递归
chessboard[row][column] = '.'; //回溯
}
}
}
bool isValid (int row, int column, vector<string>& chessboard, int n){
//检查列
for (int i = 0; i < row; i++){
if (chessboard[i][column] == 'Q'){
return false;
}
}
//检查45度
for (int i = row - 1, j = column + 1; i >= 0 && j < n; i--, j++){
if (chessboard[i][j] == 'Q'){
return false;
}
}
//检查135度
for (int i = row - 1, j = column - 1; i >= 0 && j >= 0; i--, j--)
if (chessboard[i][j] == 'Q'){
return false;
}
//否则合规
return true;
}
vector<vector<string>> solveNQueens(int n) {
result.clear();
//初始化棋盘
vector<string> chessboard(n, string(n,'.')); //**为什么要这么写?
backtracking(n, 0, chessboard);
return result;
}
};
总结
37. 解数独
题目链接
思路
本题为二维递归。
对于返回类型:
使用bool,因为找到唯一路径就直接返回值。
对于终止条件:
本题不需要,因为需要遍历整个树形结构寻找可能的叶子节点,找到立即返回。由于递归的下一层的棋盘一定比上一层棋盘多一个数,如果棋盘填满了自然就会终止。
对于单层循环搜素逻辑:
在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
另外就是判断棋盘是否合法:
- 同行是否重复
- 同列是否重复
- 9宫格里是否重复
其中第3点的解决思路是找出来行和列的开始位置,以此为基础的3个数之内就是范围。
int startRow = (row / 3) * 3; //很巧妙的求范围
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val)
return false;
}
}
代码
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 k = '1'; k <= '9'; k++){ //对该位置依次试9个数
if (isValid(i, j, k, board)){
board[i][j] = k;
if (backtracking(board)) //如果找到一组合适立刻返回
return true;
board[i][j] = '.';
}
}
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++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val)
return false;
}
}
//前面检查都通过则合规
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
总结
本题是第一次接触到二维递归,但其实仍然没有逃脱回溯三步曲的范畴。
思考清楚每一步,想清楚怎样递归获取所有值,以及是否有特殊判定情况,即可解决。