回溯
基础:
回溯常用于解决
- 组合
- 77.组合
- 17.电话号码的字母组合
- 39.组合总和
- 40.组合总和2
- 216.组合总和3
- 分割
- 131.分割回文串
- 93.复原IP地址
- 子集
- 78.子集
- 90.子集2
- 排列
- 46.全排列
- 47.全排列2
- 棋盘问题
- 51.N皇后
- 37.解数独
- 其他
- 491.递增子序列
- 332.重新安排行程
回溯其实就是暴力搜索,回溯是递归的副产品,只要有递归就有回溯
回溯三部曲:
- 确定回溯函数返回值以及参数
void backtracking(参数)
- 回溯函数终止条件
if(终止条件)
{
存放结果;
return;
}
- 回溯搜索的遍历过程
for(选择:本层集合中的元素)
{
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果;
}
整体框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
- 77:组合
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startindex)
{
if(path.size()==k)
{
result.push_back(path);
return;
}
// 当可供选择的不足时,剪枝
for(int i=startindex;i<=n-(k-path.size())+1;i++) // 这里涉及到剪枝优化
{
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back(); // 回退
}
return;
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n,k,1);
return result;
}
};
- 216:组合总和III
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int k, int target, int sum, int startIndex) // 确定要传递的参数和返回值
{
if(sum>target) // 这里设计一部分剪枝
return;
// 确定终止条件并保存结果
if(path.size()==k)
{
if(sum==target)
result.push_back(path);
return;
}
// 回溯搜索过程
for(int i = startIndex; i<=9-(k-path.size())+1;i++) // 这里涉及一部分剪枝
{
sum+=i;
path.push_back(i);
backtracking(k,target,sum,i+1);
sum-=i; // 别忘了减
path.pop_back();
}
return;
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,0,1);
return result;
}
};
- 电话号码的字母组合
时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数
空间复杂度:O(3^m * 4^n)
class Solution {
private:
const string lettermap[8] ={
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
vector<string> result;
string s;
void backtracking(const string& digits, int index)
{
if(index==digits.size())
{
result.push_back(s);
return;
}
// 确定单层递归逻辑
int dig = digits[index] - '0'; // 转化成数字
string letters = lettermap[dig-2]; // 取字符串
for(int i=0;i<letters.size();i++)
{
s.push_back(letters[i]);
backtracking(digits,index+1);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if(digits.size()==0) // 对空进行处理
return result;
backtracking(digits,0);
return result;
}
};
- 39:组合总和
这题如果需要剪枝的话,需要对数组排序,不一定会提高效率
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int index)
{
if(sum>target)
return;
// 不再是数字
if(sum==target)
{
result.push_back(path);
return;
}
// 回溯搜索过程
for(int i = index;i<candidates.size();i++) // 这里剪枝少算一层 && sum+candidates[i]<=target
{
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i); // 注意这里i不需要+1
sum-=candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
// 注意这题需要先排序,否则没办法进行剪枝
// sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0);
return result;
}
};
- 40:组合总和II
// 这题关键是里面存在重复的元素,所以需要添加一个map来记录,用数组即可
// 每个数字在每个组合中只能使用一次,在同一层上进行去重
// 所以可以先对数组排序,然后就可以方便对相同的元素进行去重
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int index, vector<bool> used)
{
if(sum>target)
return;
if(sum==target)
{
result.push_back(path);
return;
}
// 单层递归逻辑
for(int i=index; i<candidates.size() && sum+candidates[i]<=target; i++) // 这里有一部分剪枝
{
// candidates[i]==candidates[i-1]即左边的分支已经用过了
// used[i-1] == false说明当前是在树枝上,这时不需要去重,这个蛮难理解,用于区分是在同一层还是同一枝
if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false)
continue;
used[i] = true;
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i+1,used); // 注意因为不能重复
sum-=candidates[i];
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0,used);
return result;
}
};
分割
- 131:分割回文串
class Solution {
private:
vector<vector<string>> result;
vector<string> path;
bool isPalindrome(const string& s, int start, int end)
{
for(int i=start, j=end; i<j; i++,j--)
{
if(s[i]!=s[j])
return false;
}
return true;
}
void backtracking(const string& s, int index)
{
if(index>=s.size())
{
result.push_back(path);
return;
}
// 单层搜索逻辑
for(int i=index; i<s.size();i++)
{
if(isPalindrome(s,index,i))
{
string str = s.substr(index,i-index+1); // 取出子串
path.push_back(str);
}
else
continue;
backtracking(s,i+1);
path.pop_back();
}
}
public:
vector<vector<string>> partition(string s) {
backtracking(s,0);
return result;
}
};
- 93.复原ip地址
// 时间复杂度O(3^4)
// 空间复杂度O(n)
class Solution {
private:
vector<string> result;
bool isValid(const string& s, int start, int end)
{
if(start>end)
return false;
if(s[start]=='0' && start!=end) // 开头为0并且不止一位数
return false;
int num=0;
for(int i=start;i<=end;i++) // 不能大于255
{
if(s[i]>'9' || s[i]<'0') return false;
num = num*10+(s[i]-'0');
}
if(num>255)
return false;
return true;
}
void backtracking(string& s, int index, int num)
{
if(num==3) // 已经插入3个.
{
if(isValid(s,index,s.size()-1)) // 需要检查第四段是否合法
result.push_back(s);
return;
}
// 单层搜索逻辑
for(int i=index;i<s.size();i++)
{
if(isValid(s,index,i)) // 检查[index,i]区间是否合法
{
s.insert(s.begin()+i+1,'.'); // 插入.
num++;
backtracking(s,i+2,num); // s[i]是本段的最后一个元素,s[i+1]是.
num--; // 回退
s.erase(s.begin()+i+1);//把插入的.删除
}
else
break;
}
}
public:
vector<string> restoreIpAddresses(string s) {
if(s.size()<13) // 容易忽视
backtracking(s,0,0);
return result;
}
};
子集问题
组合问题和分割问题都是收集树的叶子节点,而子集问题是找到树的所有节点。
- 78:子集
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int index)
{
// 因为每个节点都存,这里不需要判断index<nums.size();
result.push_back(path);
// 单层搜索逻辑
for(int i=index;i<nums.size();i++)
{
path.push_back(nums[i]);
backtracking(nums,i+1); // 传i+1
path.pop_back();
}
return; // 在这返回
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return result;
}
};
- 90:子集II
// 提高难度,需要排除重复的,所以引入一个map,但是需要先对nums排序,和前面的排列组合问题一个思路
// 排序O(nlogn),子集最多2^n个,每个子集加入答案时需要拷贝一份,耗时O(n),最后时间复杂度为O(n*2^n);
// 空间复杂度O(n)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int index, vector<bool>& used)
{
result.push_back(path);
// 单层搜索逻辑
for(int i=index; i<nums.size(); i++)
{
// nums[i]==nums[i-1]说明左边的分支用过了
// used[i-1]==flase说明当前不是在树枝上,用于区分在同一层还是同一树枝
// 因为假设第二层选了[1,2],这棵树继续选2是合法的,这个时候used[i-1]==true,但是在[1,2]同一层继续选[1,2]就不合法了,这个时候used[i-1]==false;
if(i>0 && nums[i]==nums[i-1] && used[i-1]==false)
continue;
used[i] = true;
path.push_back(nums[i]);
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
return;
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums,0,used);
return result;
}
};
排列问题
排列问题也是递归到叶子节点
- 46:全排列
// 时间复杂度O(n*n!)
// 空间复杂度O(n)
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++)
{
if(used[i]==true)
continue;
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return result;
}
};
- 47:全排列II
// 时间复杂度O(n*n!)
// 空间复杂度O(n)
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]==false 确保了重复是出现在同一层, used[i]==true用于防止同一个元素被二次选择
if((i>0 && nums[i]==nums[i-1] && used[i-1]==false) || used[i]==true)
continue;
used[i] = true;
path.push_back(nums[i]);
backtracking(nums,used);
used[i] = false;
path.pop_back();
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return result;
}
};
棋盘问题
- 51:N皇后
class Solution {
private:
vector<vector<string>> result;
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;j>=0&&i>=0;j--,i--)
{
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;
}
void backtracking(int row, int n, 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(row+1,n,chessboard);
chessboard[row][col] = '.'; // 回退
}
}
}
public:
vector<vector<string>> solveNQueens(int n) {
vector<string> chessboard(n,string(n,'.'));
backtracking(0,n,chessboard);
return result;
}
};
- 37:解数独
class Solution {
private:
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;
}
// 判断3*3宫格内是否重复
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;
}
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++)
{
if(isValid(i,j,k,board))
{
board[i][j] = k;
if(backtracking(board)) return true; // 找到符合的解,直接返回
board[i][j] = '.';
}
}
return false; // 9个数都试完毕,直接返回,这就是为什么没有写终止条件也不会无限递归
}
}
return true; // 没有返回false,说明找到了符合的,就返回true
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
其他
- 491:递增子序列
这题由于nums是无序的,不能对其排序,所以无法使用子集II那种方法来排除重复,用set来做或者数组来做是更好的选择,u1s1,也更好理解
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int index)
{
// 第二层开始的所有节点
if(path.size()>1) // 将合法的结果存
result.push_back(path);
// unordered_set<int> used;
// 用数组更好
vector<bool> used(201,false);
// 单层搜索逻辑,也要考虑重复问题
for(int i=index; i<nums.size(); i++)
{
// if((!path.empty() && nums[i]<path.back()) || used.find(nums[i])!=used.end()) // 本层已经用过了
if((!path.empty() && nums[i]<path.back()) || used[nums[i]+100]==true) // 本层已经用过了
continue;
// used.insert(nums[i]); // used是在每层新建的
used[nums[i]+100] = true;
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
// vector<bool> used(nums.size(),false); // 惯性思维,nums是乱序的呀
backtracking(nums,0);
return result;
}
};
- 332:重新安排行程(hard),难啊
// 这个题可以用回溯来解决
// 需要借助一个映射来记录起点和终点,并且要记录从起点到终点的剩余票数
// unordered_map<string, map<string, int>> targets; //unordered_map<出发机场,map<到达机场,航班数>>
class Solution {
private:
// 要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效。
// 选择了unordered_map<string, map<string, int>> targets 来做机场之间的映射。
unordered_map<string,map<string,int>> targets;
bool backtracking(int ticketNum, vector<string>& result)
{
if(result.size() == ticketNum+1) // 结束行程时,经过的机场个数==航班数+1
return true;
// 单层遍历逻辑
for(pair<const string, int>& target : targets[result[result.size()-1]]) // 遍历所有以result最后一个机场为起点的所有航班,这里用到迭代器
{
// 判断一下这个航班是否飞过
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) {
vector<string> result;
// 建立映射关系
for(const vector<string>& vec:tickets)
targets[vec[0]][vec[1]]++;
result.push_back("JFK"); // 别忘了定义初始
backtracking(tickets.size(),result);
return result;
}
};