回溯算法框架
回溯算法解题套路框架
其实回溯算法其实就是我们常说的 DFS 算法,本质上就是一种暴力穷举算法。
解决一个回溯问题,实际上就是一个决策树的遍历过程。
站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
回溯算法框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
一、全排列问题
力扣第 46 题「 全排列」就是给你输入一个数组 nums
,让你返回这些数字的全排列。
解题代码:
class Solution {
public:
//回溯
vector<vector<int>>res;//存储所有可能排列
vector<vector<int>> permute(vector<int>& nums) {
vector<bool>used(nums.size(),false);//记录是否走过这个数
vector<int>track;//记录路径
backtrack(nums,track,used);
return res;
}
void backtrack(vector<int>nums,vector<int>&track,vector<bool>&used){
//结束条件
if(track.size()==nums.size()){
res.push_back(track);
return;
}
//遍历
for(int i=0;i<nums.size();i++){
if(used[i]==true){//如果这个数已经被选择过
continue;
}
used[i]=true;//选择这个数
track.push_back(nums[i]);//记入路径
backtrack(nums,track,used);//对其他数进行遍历
used[i]=false;//撤销选择
track.pop_back();
}
}
};
本质上就是暴力穷举,要深入理解【决策树】这一概念。在这里不做深入解释,想要了解可以去回溯算法解题套路框架 :: labuladong的算法小抄 (gitee.io)自行了解。
注意:
但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
二、N 皇后问题
力扣第 51 题「 N 皇后」就是这个经典问题,简单解释一下:给你一个 N×N
的棋盘,让你放置 N
个皇后,使得它们不能互相攻击。
class Solution {
public:
vector<vector<string>>res;
vector<vector<string>> solveNQueens(int n) {
vector<string>board(n,string(n,'.'));
backtrack(0,board);
return res;
}
void backtrack(int row,vector<string>&board){
//结束条件
if(row==board.size()){//当选择完的行数等于棋盘的行数时
res.push_back(board);
return;
}
//遍历 一行中的每个数也就是改变列即可
for(int col=0;col<board.size();col++){
//除去不合法的选择
if(!isValid(board,row,col)){
//不符合游戏规则 直接跳过
continue;
}
//符合规则选择
board[row][col]='Q';
//对下一行进行选择
backtrack(row+1,board);
//撤销选择
board[row][col]='.';
}
}
bool isValid(vector<string>&board,int row,int col){
int n=board.size();
//检查一列中是否有冲突
for(int i=0;i<row;i++){
if(board[i][col]=='Q')return false;
}
//检查右上是否有冲突
for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
if(board[i][j]=='Q')return false;
}
//检查左上是否有冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q')
return false;
}
return true;
}
};
PS:肯定有读者问,按照 N 皇后问题的描述,我们为什么不检查左下角,右下角和下方的格子,只检查了左上角,右上角和上方的格子呢?
因为皇后是一行一行从上往下放的,所以左下方,右下方和正下方不用检查(还没放皇后);因为一行只会放一个皇后,所以每行不用检查。也就是最后只用检查上面,左上,右上三个方向。
三、最后总结
回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:
def backtrack(...):
for 选择 in 选择列表:
做选择
backtrack(...)
撤销选择
写 backtrack
函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。
其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。
本文来自博客园,作者:{BailanZ},转载请注明原文链接:https://www.cnblogs.com/BailanZ/p/16070485.html