前面几篇博客我们学会了使用回溯法解决一些中等难度的问题,现在要学习其更困难的题目。因为图论中也运用了回溯法的思想,所以我们现在用回溯法解决一些图论中有一定难度的题目。
一、重新安排行程
例题为332. 重新安排行程 - 力扣(LeetCode),给你一份航线列表 tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
使用unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets表达映射关系。航班次数大于零,则可以飞往该目的地,等于0则已经不可以飞往该目的地。map自带排序。
然后我们利用回溯三部曲梳理一下写这题代码的思路。
首先是确认回溯函数的参数和返回值。我们需要一个一维数组存储已经遍历了的城市路径,我们还需要一个参数保存总共的机票数。当机票数+1=遍历数组内的城市数(可以理解为边+1=点)时,说明我们已经使用了所有的机票,形成了一条路径。由此,我们也完成了回溯三部曲的第二部,确定了返回条件。但这里需要注意的是,这次回溯函数需要返回值。因为这次只需要返回一条路径,如果找到这条最好的路径需要直接返回,所以我们需要一个返回值来表示我们是否找到该路径,因此本题回溯函数需要返回bool。
第三步是具体单层的逻辑。对于每一层,我们用for循环遍历路径的最后一个城市可以到达的所有目的地,如果还可以到达该目的地,那就把该城市push进路径里,然后展开回溯。如果回溯后返回值为1,说明这条路可以得到最佳答案,就直接返回。如果不是,则pop出该城市。
具体代码如下:
class Solution {
public:
unordered_map<string,map<string,int>> targets; //存放出发机场, map<到达机场, 航班次数>
bool backtracking(int tiknum, vector<string>& result)
{
if(result.size()==tiknum+1)//n张机票连接n+1个城市
{
return 1;
}
for(pair<const string, int>& target: targets[result[result.size()-1]])//targets里的最后一个城市可到达的所有目标城市以及对应次数的pair
{
if(target.second>0)//还可以到达该目的地
{
result.push_back(target.first);
target.second--;
if(backtracking(tiknum,result))//为真表示已经找到满足条件的情况了则可以直接返回
{
return true;
}
result.pop_back();
target.second++;
}
}
return 0;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(const vector<string>& vec: tickets)
{
targets[vec[0]][vec[1]]++;
}
vector<string> result;
//初始的起点机场
result.push_back("JFK");
backtracking(tickets.size(),result);
return result;
}
};
二、N皇后
回溯法可以解决棋盘问题。N皇后是非常经典的棋盘问题,例题为51. N 皇后 - 力扣(LeetCode),按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n
,返回所有不同的n皇后问题 的解决方案。每一种解法包含一个不同的n 皇后问题的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
我们可以用回溯遍历棋盘每一行的皇后放在第几个格子上。树的深度由行数决定,树每层的分叉的数由列数决定。
使用回溯三部曲。第一步,确定回溯函数参数和返回值。首先这个题不是只返回一个最佳结果,所以不需要返回值。参数我们需要棋盘的大小n,以及遍历到第几行了,以及现在的棋盘情况。void backtracking(int n, int row, vector<string>& board);
第二步,返回条件。当行数row==n时说明已经遍历了n行了,所以结束了所有的遍历,即可将该棋盘Push进result,并return。
第三步,单层遍历逻辑。对于单层就是每一行,我们需要遍历该行的每一列的位置,判断Q放在这个列是否可行,不冲突的话就放Q在这一列,然后调用回溯函数,继续下一行,回溯结束后需要将这列恢复'.'。
整体代码如下:
class Solution {
public:
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& board)//row为遍历到第几行了,即树的深度
{
if(row==n)//最后一行结束了
{
result.push_back(board);
return;
}
for(int i = 0; i<n; i++)//遍历这一行的皇后放在哪一列
{
if(isValid(row,i,board,n))//符合条件
{
board[row][i] = 'Q';
backtracking(n,row+1, board);
board[row][i] = '.';
}
}
}
bool isValid(int i, int j, vector<string> board, int n)
{
for(int p = 0;p<i; p++)//检查每一行上的对应列是否有Q
{
if(board[p][j]=='Q')
{
return 0;
}
}
//斜左上
for(int p = i-1,q = j-1; p>=0&&q>=0; p--,q--)
{
if(board[p][q] == 'Q')
{
return false;
}
}
//斜右上
for(int p = i-1, q = j+1; p>=0&&q<n; p--,q++)
{
if(board[p][q] == 'Q')
{
return false;
}
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<string> board(n,string(n,'.'));
backtracking(n,0,board);
return result;
}
};
三、解数独
例题为37. 解数独 - 力扣(LeetCode),编写一个程序,通过填充空格来解决数独问题。
本题跟N皇后类似,都是棋盘类题目。但是仔细一想会发现比N皇后多了一个维度。因为N皇后是遍历每一行,然后每一层去遍历每一列是否可以放Q。但是棋盘需要遍历每一列是否可以放1-9,比N皇后多了一层1-9的遍历,所以我们需要两层for循环。在确定了行列的情况下还要遍历1-9,确认是否可以填在该位置。
然后我们使用回溯三部曲写代码。
首先确认回溯函数的参数和返回值。因为本题只用返回一个结果,所以需要返回值Bool。参数需要棋盘作为参数。
然后确认返回条件,如果遍历完每一个格子都没有问题,则返回true。
最后确定单层逻辑,遍历每一行每一列看每一个格子,如果没被填写则遍历1-9进行回溯填写。如果不冲突,则填上,然后进入回溯,如果回溯返回true说明这么填可以得到最后正确结果,所以返回true。如果不是true的返回,则将该位置该回'.',继续for循环的遍历。
整体代码如下:
class Solution {
public:
bool backtracking(vector<vector<char>>& board)
{
for(int i = 0; i<board.size(); i++)
{
for(int j = 0; j<board.size(); j++)
{
if(board[i][j]=='.')
{
for(int p = 1; p<=9; p++)
{
if(isValid(i,j,p,board))
{
board[i][j] = '0'+p;
if(backtracking(board)) return true;
board[i][j] = '.';
}
}
return false;//1-9遍历完了都不行
}
}
}
return true;//每一行每一列都ok了
}
bool isValid(int i, int j,int p, vector<vector<char>>& board)
{
for(int q = 0; q<board.size(); q++)
{
if(board[i][q] == '0'+p) return false;
if(board[q][j] == '0'+p) return false;
}
// 判断9方格里是否重复
int startRow = (i / 3) * 3;
int startCol = (j / 3) * 3;
for (int m = startRow; m < startRow + 3; m++)
{
for (int n = startCol; n < startCol + 3; n++)
{
if (board[m][n] == '0'+p )
{
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
说明:本文为作者整理知识点用于复习巩固,参考了代码随想录的讲解,有问题可以联系作者欢迎讨论~