面试常考算法题---回溯法(学习笔记)
Posted 忆_恒心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试常考算法题---回溯法(学习笔记)相关的知识,希望对你有一定的参考价值。
前言:
学习网站:
- 代码随想录---算法讲解
- LeetCode官网作为刷题的平台
回溯法小结
回溯算法可以解决的问题如下:
- 组合问题:N个数里面按照一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独
框架:
void backtracking(参数)
if (终止条件)
存放结果;
return;
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小))
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
组合问题
抽象成树结构
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
优化回溯
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
组合总和
组合总和(一)
在组合问题上加了一个元素总和的限制,不可重复取
优化剪枝
本题的剪枝会好想一些,即:已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉,
还可以再剪:
i <= 9 - (k - path.size()) + 1
组合总和(二)
可以重复取,回溯的时候不用startIndex+1
树形结构:
剪枝优化
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
组合总和(三)
集合里有重复,但要求解集不能包含重复的组合。
提到了树枝去重和树层去重
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
多个集合求组合
在回溯算法:电话号码的字母组合 (opens new window)中,开始用多个集合来求组合,还是熟悉的模板题目,但是有一些细节。
树形结构如下:
切割问题
难点二在于如何截取
void backtracking(const string s, int startIndex)
if(startIndex >= s.size())
res.push_back(path);
return ;
for(int i = startIndex; i < s.size(); ++i)
if(isPart(s, startIndex, i))
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
backtracking(s, i + 1);
path.pop_back();
else
continue;
子集问题(一)
与组合问题的区别在于,在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
子集问题(二)
回溯算法:求子集问题! 对子集问题去重.
:fire:注意一定要先排序,这样才可以起到去重的作用
递增子序列
也是要去重的,但是不能用之前的方法先排序再去重(递增子序列会被破坏)
考虑用set
void backtracking(vector<int>& nums, int startIndex)
// 题目要求子序列的元素要大于2
if(path.size() >= 2)
res.push_back(path);
unordered_set<int> uset;
for(int i = startIndex; i < nums.size(); ++i)
if((!path.empty() && nums[i] < path.back()) || uset.find(nums[i])!=uset.end())
// 下一层的节点比上一层子序列的最后一个还小
// 出现重复的情况
continue;
else
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
排列问题
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
这时候就不再需要startIndex了
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
排列问题(二)
排列问题要要去重了,去重的原理和组合的是一样的,但是值得注意的是:
剪枝的位置有两个地方:
used[i - 1] == false也可以(之前的方式),used[i - 1] == true也可以!
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!
class Solution
private:
vector<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used)
// 此时说明找到了一组
if (path.size() == nums.size())
result.push_back(path);
return;
for (int i = 0; i < nums.size(); i++)
// used[i - 1] == true,说明同一树枝nums[i - 1]使用过
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
continue;
if (used[i] == false)
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
public:
vector<vector<int>> permuteUnique(vector<int>& nums)
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 排序
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
;
棋盘问题
N皇后问题
N皇后问题,经典的困难问题
分析易得:
树的高度就是矩阵的高度
树中每个节点的宽度就是矩阵的宽度
约束条件:
只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置。
class Solution
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
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(row, col, chessboard, n)) // 验证合法就可以放
chessboard[row][col] = Q; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = .; // 回溯,撤销皇后
bool isValid(int row, int col, vector<string>& chessboard, int n)
int count = 0;
// 检查列
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;
public:
vector<vector<string>> solveNQueens(int n)
result.clear();
std::vector<std::string> chessboard(n, std::string(n, .));
backtracking(n, 0, chessboard);
return result;
;
解数独问题
抽象成树结构的话,其实是比较大的,选取部分进行分析:
二维递归:
难点:
递归函数的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
递归终止条件?
本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
如何进行二维递归?
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
bool backtracking(vector<vector<char>>& board)
for (int i = 0; i < board.size(); i++) // 遍历行
for (int j = 0; j < board[0].size(); j++) // 遍历列
if (board[i][j] != .) continue;
for (char k = 1; k <= 9; k++) // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board))
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = .; // 回溯,撤销k
return false; // 9个数都试完了,都不行,那么就返回false
return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
整体的实现代码:
class Solution
private:
bool backtracking(vector<vector<char>>& board)
for (int i = 0; i < board.size(); i++) // 遍历行
for (int j = 0; j < board[0].size(); j++) // 遍历列
if (board[i][j] != .) continue;
for (char k = 1; k <= 9; k++) // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board))
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = .; // 回溯,撤销k
return false; // 9个数都试完了,都不行,那么就返回false
return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
bool isValid(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;
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) // 判断9方格里是否重复
for (int j = startCol; j < startCol + 3; j++)
if (board[i][j] == val )
return false;
return true;
public:
void solveSudoku(vector<vector<char>>& board)
backtracking(board);
;
重新安排行程
class Solution
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result)
if (result.size() == ticketNum + 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(ticketNum, result)) return true;
result.pop_back();
target.second++;
return false;
public:
vector<string> findItinerary(vector<vector<string>>& tickets)
targets.clear();
vector<string> result;
for (const vector<string>& vec : tickets)
targets[vec[0]][vec[1]]++; // 记录映射关系
result.push_back("JFK"); // 起始机场
backtracking(tickets.size(), result);
return result;
;
性能分析
学习资料:
- 代码随想录
- LeetCode官网
以上是关于面试常考算法题---回溯法(学习笔记)的主要内容,如果未能解决你的问题,请参考以下文章