90. 子集 II


c++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>&nums,vector<bool> &used,int startIndex){
if(startIndex>=nums.size()) return;
for(int i=startIndex;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;//树层去重
path.push_back(nums[i]);
res.push_back(path);
used[i]=true;
backtracking(nums,used,i+1);//回溯
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
res.push_back({});
sort(nums.begin(),nums.end());//排序
vector<bool> used(nums.size(),false);
backtracking(nums,used,0);
return res;
}
};
491. 非递减子序列


同样是树层去重,那这道题和上道题的区别是什么呢?
上道题由于可以排序,所以我们可以通过判断现在的数和上一个数是否相等,而跳过循环
这道题为保证递增,无法排序,我们只知道是在一个树层上的元素不能相等,所以需要用unordered_set来记录在同一树层上取过的元素,从而完成去重
c++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>&nums,int startIndex){
unordered_set<int> uset;
for(int i=startIndex;i<nums.size();i++){
if(uset.find(nums[i])!=uset.end()) continue;//树层去重
if(path.empty()||(!path.empty()&&nums[i]>=path.back())){
path.push_back(nums[i]);
uset.insert(nums[i]);
//uset是记录出现过的元素,所以回溯时不用再删除添加进去的nums[i]
if(path.size()>1) res.push_back(path);
backtracking(nums,i+1);
path.pop_back();
}
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};
46. 全排列


明白排列与组合的区别:不需要startIndex记录起始位置,只需要used数组判断当前元素是否是否加入到排列中
c++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>&nums,vector<bool>&used){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(used[i]==true) continue;//不重复加入元素,只加入没有加入过的元素
//标准回溯,回溯函数前后对称
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};
47. 全排列 II


这道题本质上也是对重复元素进行树层去重,根据前面做过的题,我们会发现
我们既可以先排序再跳过相同元素,也可以不排序而用unordered_set出现过的元素
(这里对应方法一和方法二)
方法一:
c++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>&nums,vector<bool>&used){
if(nums.size()==path.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
//当前元素和前一个元素相同且在同一树层上
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
backtracking(nums,used);
return res;
}
};
方法二:
一次backtracking的调用,相当于在树形结构中往深/下一层的探索,所以在递归函数内部定义unordered_set,就相当于每个树层去检验重复元素
c++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>&nums,vector<bool>&used){
unordered_set<int> uset;
if(nums.size()==path.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(uset.find(nums[i])!=uset.end()) continue;
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
uset.insert(nums[i]);
backtracking(nums,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};
51. N 皇后

-
属于棋盘问题
-
每次调用回溯函数:进入棋盘的下一行
-
回溯函数中的for循环:找到皇后在该行中的位置(第几列)
-
与前面题的最大区别:前面都是在一维数组中取一个数或切割出几个数去加入到路径中,这道题我们是在二维数组中遍历一维数组去找到皇后的位置(即按行递归,每一行在多个列中做选择)
c++
class Solution {
public:
vector<vector<string>> res;
//传入二维数组棋盘,当前递归(处理)的行数
void backtracking(vector<string>&chessboard,int n,int row){
if(row==n){
res.push_back(chessboard);
return;
}
//在该行中找到所处列的位置
for(int col=0;col<n;col++){
if(isValid(row,col,chessboard,n)){
chessboard[row][col]='Q';
backtracking(chessboard,n,row+1);//回溯
chessboard[row][col]='.';
}
}
}
//减枝操作
bool isValid(int row,int col,vector<string>&chessboard,int n){
//皇后不同列:加入的皇后不能和之前的皇后在同一列上
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&&col<n;i--,j++){
if(chessboard[i][j]=='Q') return false;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<string> chessboard(n,string(n,'.'));
backtracking(chessboard,n,0);
return res;
}
};
37. 解数独


-
每次调用回溯函数:在空格处添加数字1-9
-
双侧for循环:遍历二维数组,找到空格
-
剪枝操作:判断行是否有相同元素、判断列是否有相同元素、判断3x3宫中是否有相同元素
-
和前面题的主要区别:在前面题目中我们需要找到该数组的全部子集(组合)或N皇后的所有位置,但这道题只需要找到一个满足题意的数独解法就可以
相当于:前者是搜索二叉树的全部路径(所以返回值是
void,需要结果集来存储结果),后者是找到一个满足题意的路径就可以(所以返回值是bool,找到一个直接就返回true) -
终止条件:这里没有像前面题的"路径中的元素等于数组中的元素"或"
row==n"的显示终止条件,而是把它放到回溯函数中如果
数字1-9都无法递归下去说明当前分支无法得到解,就返回false如果双层for循环遍历完都没有找到空格,说明空格被填满(即找到一个可行解),返回
true注:那
true和false应该写在什么位置呢?就应该写在上方条件的下方(举个例子:写到for循环1-9的下面,返回false),因为如果进入循环就进入递归函数了,就不会走到for循环的下面 -
递归返回值的层级:
最底层(成功终止)
当双层循环结束后没有遇到 空格,说明所有格子都已填满,此时执行
return true;。这一层代表找到了完整解,是递归的出口。· 中间层(成功传递)
当某层递归调用
backtracking(board)返回true时,意味着从该分支出发的后续填数全部成功。当前层立即return true;,将成功信号逐级向上传递,不再尝试其他数字。中间层(失败回溯)
当某层对当前空格尝试所有数字后,没有一个数字能导致后续递归成功,则执行
return false;。这表示以当前棋盘状态为起点的所有路径都行不通,上一层收到false后会撤销刚才放置的数字,继续尝试其他数字。最外层(最终结果)
solveSudoku调用backtracking(board)时,如果最终返回 true,表示数独有解;返回 false 则表示无解(本题保证有解,所以一定是 true)。
c++
class Solution {
public:
bool backtracking(vector<vector<char>>&board){
//双层for循环遍历数独表格
for(int i=0;i<board.size();i++){
for(int j=0;j<board[0].size();j++){
//找到空格
if(board[i][j]=='.'){
//尝试数字1-9哪个可以
for(char k='1';k<='9';k++){
//找到可以的数字
if(isVaild(i,j,k,board)){
board[i][j]=k;
if(backtracking(board)) return true;//满足题意直接返回
board[i][j]='.';//否则继续回溯
}
}
return false;
}
}
}
return true;
}
bool isVaild(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;
}
//判断3x3宫
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);
}
};