代码随想录训练营Day30 | Leetcode 332、 51、37
一、332 重新安排行程
题目链接:332 重新安排行程
核心:
首先需要一个map记录出发机场和到达机场(包括到达机场的次数)的映射关系,一个机场可以映射多个机场,机场之间是按照字母顺序排列的,前者可以使用unordered_map结构,后者可以使用map或multimap或multiset结构。此处需使用unordered_map和map结构,即:unordered_map<出发机场,map<到达机场,航班次数>> targets;使用map的原因是可以使用航班次数对出现的机场进行增减,以标记到达机场是否使用过了。若航班次数大于0说明到达机场是可以抵达的,若等于0说明到达机场已经飞过来,不能再次抵达了。
注意:这里只要找到一条合适的叶子节点即可结束,因此返回类型是bool。
//unordered_map<出发机场,map<到达机场,航班次数>>
unordered_map<string,map<string,int>> targets;
bool backtracking(vector<string>& res,int ticketNum)
{
if(res.size()==ticketNum+1)
return true; //终止条件:途径机场数=航班次数+1
for(pair<const string, int>& target:targets[res[res.size()-1]])
{
if(target.second>0)
{//记录此机场是否已经飞过了,若小于等于则表示不能飞
res.push_back(target.first);
target.second--;
if(backtracking(res,ticketNum))
return true;
target.second++;
res.pop_back(); //回溯,撤销记录的机场
}
}
return false;//遍历完所有机场都没有返回true说明无有效途径
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> res;
res.push_back("JFK"); //起始机场
//targets的初始化,出发和到达机场映射到targets
for(const vector<string>& vec: tickets)
targets[vec[0]][vec[1]]++; //map
backtracking(res,tickets.size());
return res;
}
二、51 N皇后
题目链接:51 N皇后
核心:每行只需放置一个皇后,可以从第0行开始进入回溯函数,遍历所有的列,检查是否存在可以放置皇后的位置,因此这是一个一维回溯问题:棋盘的高是树的高度,棋盘的宽度是树中每个节点的宽度(每一层的节点数)。
1、参数和返回值:棋盘的大小n,当前遍历到棋盘的行数以及每层棋盘的放置情况,返回值是void(此题是只要找到一个合适的叶子节点就返回,为何不使用bool类型作为返回值?)
另外,需要定义一个全局变量:记录最终返回的结果,即每一层的棋盘集合。
2、终止条件:当遍历的行数等于棋盘大小时说明已经遍历完棋盘的所有行,此时可以退出,并且记录当前层的棋盘放置结果到res;
3、单层回溯的搜索逻辑:递归深度是row,即棋盘的行,for循环里需使用col控制当前row的所有列,检查在当前行row的情况下,是否存在某一合适位置是有效的,可以放置皇后,如果找到了就递归到下一行继续遍历寻找合适的位置。
4、判断棋盘放置皇后位置的有效性:
第一,不能同行;(for循环里已固定了某一row,即每层递归只会选择同行的一个元素,不存在同行元素重复的情况)
第二,不能同列;
第三,不能同斜对角(45°和135°)。(45° 和135° 的判断是不同的,需要注意并理解。)
vector<vector<string>> res;
void backtracking(int n,int row,vector<string>& chess)
{//row:遍历到棋盘的某行,chess:每行棋盘的放置方案
if(row==n)
{//终止条件:从棋盘第1行开始(row=0),直到遍历结束(row=n)
res.push_back(chess);
return;
}
for(int col=0;col<n;++col)
{//遍历列数,每一列都需要从头开始
if(isValid(row,col,chess,n))
{//位置row,col有效,允许放置Q
chess[row][col]='Q'; //放置Q
backtracking(n,row+1,chess); //递归到下一层,即row+1
chess[row][col]='.'; //回溯,取消放置的Q
}
}
}
bool isValid(int row,int col,vector<string>& chess,int n)
{//判断某位置[row,col]是否有效
for(int i=0;i<row;++i)
{//检查同列是否存在Q
if(chess[i][col]=='Q')
return false;
}
for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--)
{//检查45°是否存在Q
if(chess[i][j]=='Q')
return false;
}
for(int i=row-1,j=col+1;i>=0 && j<n;i--,j++)
{//检查135°是否存在Q
if(chess[i][j]=='Q')
return false;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<string> chess(n,string(n,'.')); //n=4,初始化为{....}
backtracking(n,0,chess);
return res;
}
三、37 解数独
题目链接:37 解数独
核心:解数独是一类二维回溯的问题
1、参数和返回值:
目的是填满棋盘,因此输入参数只有棋盘;只要任一条树枝(叶子节点)能填满棋盘即可退出,因此这是一个返回bool类型的函数。
2、终止条件:无需终止条件,因为只要找到一个符合要求的叶子节点就立即退出,即便没有找到符合要求的也会退出,依次没有终止条件也不会陷入无限循环中。
3、单层回溯的逻辑
二层循环遍历,一层遍历行,一层遍历列;在当前位置是空白格时,检查1-9这9个数字是否符合填入棋盘的要求(行、列和9宫格不存在重复元素),如果有效则填入数字并递归遍历棋盘,若9个数字都无效则返回false。最后遍历完整个棋盘都都没有返回false,说明已找到合适的叶子节点,返回true退出函数。
4、判断某数字填入棋盘的有效性
第一,遍历行不存在重复元素;
第二,遍历列不存在重复元素;
第三,遍历9宫格内所有元素不存在重复元素。(要学习并理解的代码的实现)
bool backtracking(vector<vector<char>>& board)
{//回溯函数,无需终止条件,返回类型bool是因为只要找到一个符合要求的叶子节点即可返回
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++)
{//依次遍历1-9,check该位置填入数字的有效性
if(isValid(i,j,k,board))
{
board[i][j]=k; //填入数字k
if(backtracking(board))
return true; //一旦递归到某个叶子节点符合要求,即返回true
board[i][j]='.'; //回溯,撤销填入的数字,依然置为空白格
}
}
return false;
}
}
}
return true; //遍历完棋盘的每个位置都没有返回false,说明已找到合适的叶子节点
}
bool isValid(int row,int col,char ch,vector<vector<char>>& board)
{
for(int i=0;i<9;++i)
{//遍历此行是否存在重复元素
if(board[row][i]==ch)
return false;
}
for(int j=0;j<9;++j)
{//遍历此列是否存在重复元素
if(board[j][col]==ch)
return false;
}
int startx=(row/3)*3;
int starty=(col/3)*3;
for(int i=startx;i<startx+3;++i)
{//遍历9宫格内是否存在重复元素
for(int j=starty;j<starty+3;++j)
if(board[i][j]==ch)
return false;
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}