第七章 回溯算法part06
332. 重新安排行程
给你一份航线列表 tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]
与["JFK", "LGB"]
相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
思路:回溯,对于实现没什么思路,主要要解决这几个问题:
1、选择航线时实现按字典排序;
2、使用什么数据结构来对航线信息进行记录包括是否使用过该航线;
3、终止条件为所有航线都被使用过。
看了题解后,解决方法如下:
1、使用map对航线进行排序;
2、使用unordered_map<string,map<string,int>>target记录航线信息以及对使用过该航线进行记录。字符串可以直接作为索引!
class Solution {
public:
vector<string> result;
//记录航线信息
unordered_map<string,map<string,int>>targets;
//回溯函数有返回值
bool backtracking(int ticNum,vector<string>& result){
//终止条件
if(result.size()==ticNum+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(ticNum,result)) return true;
result.pop_back();
target.second++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
//初始条件,将航线信息记录到targets中
for(const vector<string>&vec:tickets){
targets[vec[0]][vec[1]]++;
}
result.push_back("JFK");
backtracking(tickets.size(),result);
return result;
}
};
实现过程中还有一个重要问题即回溯函数在这里需要设置返回值,因为算法在遇到第一个解之后就会停止查找,所以需要通过返回值来限制。
51. N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
思路:逐个空格遍历,时间复杂度应该是n^n,感觉很复杂无从下手。看了题解,使用逐层遍历,因为每一行只能有一个皇后,其实应该算是一次剪枝吧。问题就变成在每一行中找到N皇后的位置,通过判断它所在列以及所在斜线是否已经有皇后还找到每一行中的合适位置。于是可以套回溯的解题思路。
至于最终的表示结果,使用的是vector<vector<string>>数据结构来存储,感觉也是很巧妙,初始化方法一开始写不出来,需要重点掌握,string的访问方法。
class Solution {
public:
vector<vector<string>> result;
vector<string> chessboard;
bool isValid(int n,int row,int col,vector<string>& chessboard){
//检查列不冲突
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;
}
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(n,row,col,chessboard)){
chessboard[row][col]='Q';
backtracking(n,row+1,chessboard);
chessboard[row][col]='.';
}
}
}
vector<vector<string>> solveNQueens(int n) {
//错误初始化做法
// for(int i=0;i<n;i++){
// for(int j=0;j<n;j++){
// chessboard[i][j]='.';
// }
// }
vector<string> chessboard(n,string(n,'.'));
backtracking(n,0,chessboard);
return result;
}
};
- 时间复杂度: O(n!)
- 空间复杂度: O(n)
37. 解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
思路:与N皇后有相似之处,应该是逐个空格遍历,无法做一个剪枝(自己这么理解)。看了题解才想明白就是每个空格逐个试1~9,然后对每一个空格做遍历,两个for循环嵌套着递归。
回溯函数有返回值,因为题目只有唯一解,找到一个解就可以终止回溯过程。
没有终止条件!!
class Solution {
public:
bool isValid(int row,int col,char k,vector<vector<char>>& board){
//检查行
for(int i=0;i<9;i++){
if(board[row][i]==k) return false;
}
//检查列
for(int j=0;j<9;j++){
if(board[j][col]==k) return false;
}
//检查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]==k) return false;
}
}
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 k='1';k<='9';k++){
if(isValid(i,j,k,board)){
board[i][j]=k;
if(backtracking(board)) return true;
board[i][j]='.';
}
}
return false; //9个数都试了不行则找不到解
}
}
return true; //遍历完每一个位置没有false则说明找到解
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
return false的位置:如果有一个空格1~9数字都不行则说明该问题无解,会直接返回。这也是为什么没有终止条件棋盘也不会永远填不满棋盘无线递归下去!
上面三个问题看了题解感觉都不是很难,主要是每个题都有一个创新点,二刷争取不看题解一遍过
总结回溯算法:每个问题都从三部曲去分析