文章目录
回溯算法
所有的回溯法解决问题可以抽象为树形结构
DFS模板
BFS模板
组合
vector.pop_back()
还可以剪枝
在求和问题中,排序之后加剪枝是常见的套路!
组合求和III
优化后的代码
class Solution {
public:
vector<vector<int>> arr;
vector<int> path;
void DFS(int a, int k, int n, int sum){
if(path.size() >= k){ //其实只会== ,>=更健壮一点
if(sum > n){
return ;
}
if(sum == n){
arr.push_back(path);
}
return ;
}
for(int i=a;i<=9-(k-path.size())+1;i++){ //剪枝 不要忘了加一
path.push_back(i);
DFS(i+1,k,n,sum+i); //sum+i直接放到参数里,也是另一种意义上的回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
DFS(1,k,n,0);
return arr;
}
};
电话号码的字母组合
和上面两道题不一样的地方
这句话得慢慢品
其实for就是当前能走的选择,在递归就是迈出那一步
组合总和
优化如下
**组合总数
难点
去重思路
名词:树层去重 和 树枝去重
这里是树层去重
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
int used[105];
long long sum = 0;
void DFS(int ind, vector<int>& arr, int tar){
if(sum > tar) return ;
if( sum == tar){
ans.push_back(path);
return ;
}
for(int i=ind;i<arr.size();i++){
if(i > 0 && arr[i] == arr[i-1] && used[i-1] == 0 ){ //树枝去重!
continue;
}
path.push_back(arr[i]);
sum += arr[i];
used[i] = 1; //标记使用过
DFS(i+1, arr, tar);
path.pop_back();
sum -= arr[i];
used[i] = 0;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
for(int i = 0;i<100;i++) used[i] = 0;
sort(candidates.begin(), candidates.end()); //排序是为了让相等的元素靠在一起
DFS(0, candidates, target);
return ans;
}
};
关于 const string&
所以我们无脑直接加吧
分割回文串
切割问题
好像遇到不懂的回溯题,画个树形图会跟好理解
难点
- 如何切割子串 – ind 到 i;
注意substr函数的参数
画图就好理解多了
复原IP地址
和上一题很像,都是切割问题
class Solution {
public:
vector<string> ans;
string path;
int cnt = 0;
int work(const string& s){
if(s.size() > 3) return 0;
int val = 0, t = 1;
for(int i=s.size()-1;i>=0;i--){
val += t*(s[i] - '0');
t *= 10;
}
if(val > 255) return 0;
if(s.size() > 1 && s[0] == '0') return 0;
return 1;
}
void DFS(int ind, const string& s){
if(ind >= s.size() && cnt == 4){ //切割到末尾 & 正好是4段
ans.push_back(path);
return;
}
for(int i=ind; i<s.size(); i++){
string ss = s.substr(ind,i-ind+1);
string oldp; //用于回溯
if( work(ss) == 1){
cnt++; //统计分成了几段
oldp = path;
if(path.size() != 0) path.push_back('.');
for(int j=0; j<ss.size();j++)
path.push_back(ss[j]);
}else {
continue;
}
DFS(i+1, s);
path = oldp;
cnt--;
}
}
vector<string> restoreIpAddresses(string s) {
if (s.size() < 4 || s.size() > 12) return ans; // 算是剪枝了
DFS(0, s);
return ans;
}
};
子集
送分题
子集问题,在树形结构中子集问题是要收集所有节点的结果,
而组合问题是收集叶子节点的结果。
子集II
树层去重 + 子集
非递减子序列
关键是在不排序的情况下如何去重
这也是需要注意的点,unordered_set<int> uset;
是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!
刚刚测试了一下,之前排序后的数组使用used的方法 可以 用 sett去重来替代 – 不过set的效率肯定手没有数组快的
借助set – set.find() == set.end(), set.insert()
全排列
其实参数那里可以不用ind, 但是加上也不影响
全排列II
从两个方面去重
一个是树枝去重,一个是全排列去重
老师的代码
其实一个用used数组就可以了
时间复杂度
.
关于集合内值的排序
重新安排行程
难点:
- 字母排序
- 设计数据结构
- 如何遍历 – 增强for循环
- pair的用法
只有一个答案的时候且拿路径作为答案返回的时候,要小心答案不要被回溯了
class Solution {
public:
// 起飞位置 到达位置 次数
map<string, map<string, int>> mapp; //对应key相同的元素,自动按字母升序排序
vector<string> path;
int mark = 0;
void DFS(int cnt){
if(mark == 1) return ;
if(path.size() >= cnt + 1 ){
mark = 1;
return ;
}
//遍历
for(pair<const string, int>& target : mapp[path.back()]){ //path里面最后一个元素就是下一个航班的起飞位置
if(target.second > 0){ //是否还有次数
path.push_back(target.first);
target.second--;
DFS(cnt);
if(mark == 1) return ; //一定要加上这个 ,不然答案记录答案后 被后面的逻辑给回溯了 就没有了
path.pop_back();
target.second++;
}
}
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
//对次数进行预处理
for(vector<string>& vec : tickets ){
mapp[vec[0]][vec[1]]++;
}
//从JFK开始飞
path.push_back("JFK");
DFS(tickets.size());
return path;
}
};
N皇后
-
初始化 vector path(n,string(n,‘.’));
-
理解path,和ans的关系 – 一定要提前想清楚
如何初始化
vector path(n,string(n,‘.’));
class Solution {
public:
vector<vector<string>> ans;
int work(int n, int han, int lei, vector<string>& path ){
//判断垂直方向
for(int i = han -1; ;i--){
if(i < 0) break;
if(path[i][lei] == 'Q') return 0;
}
//左上方
for(int i = han-1, j = lei-1; ;i--,j--){
if(i < 0 || j < 0) break;
if(path[i][j] == 'Q') return 0;
}
//右上方
for(int i = han-1, j = lei+1; ; i--,j++){
if(i < 0 || j >= n) break;
if(path[i][j] == 'Q') return 0;
}
return 1;
}
void DFS(int n,int han, vector<string>& path ){
if(han >= n){
ans.push_back(path);
return ;
}
for(int lei = 0; lei < n; lei++){
if(work(n,han,lei,path) == 1){
path[han][lei] = 'Q';
DFS(n,han+1,path);
path[han][lei] = '.';
}
}
}
vector<vector<string>> solveNQueens(int n) {
//初始化
vector<string> path(n,string(n,'.'));
DFS(n,0,path);
return ans;
}
};
解数独
难点
- 如何判断是否遵循条件
- 如何递归 – 二维递归
class Solution {
public:
int mark = 0;
int work(int han,int lei, vector<vector<char>>& board){
//垂直方向
for(int i = 0;i<board.size(); i++){
if(i == han) continue;
if(board[i][lei] == board[han][lei]) return 0;
}
//水平
for(int i = 0;i<board.size(); i++){
if(i == lei) continue;
if(board[han][i] == board[han][lei]) return 0;
}
//小正方形 -- 数学问题 ,找左上角
int x = (han/3) * 3;
int y = (lei/3) * 3;
for(int i = x; i<x+3; i++){
for(int j = y; j<y+3; j++){
if( i == han && j == lei) continue;
if(board[i][j] == board[han][lei]) return 0;
}
}
return 1;
}
void DFS(vector<vector<char>>& board){
if(mark == 1) return ;
//因为每一层不止修改一个元素 所以我们这里 二维递归
for(int i=0;i<board.size();i++){
for(int j=0;j<board[0].size();j++){
if(board[i][j] == '.'){
for(int k=1;k<=9;k++){
board[i][j] = '0' + k;
if(work(i,j,board) == 1){
DFS(board);
if(mark == 1) return; //防止找到答案后被回溯了
}
board[i][j] = '.';
}
return ;
}
}
}
mark = 1;
return ;
}
void solveSudoku(vector<vector<char>>& board) {
DFS(board);
}
};
回溯大总结
常见剪枝
什么时候需要startindex
去重方式有两种
在使用used去重时一定要先排序
思维导图
此文章用于笔者记录学习,也希望对你有帮助