回溯:
在分享Leetcode这些经典回溯算法题之前,我们首先来聊聊什么是回溯算法。回溯算法就是一个暴力枚举,搜索尝试的过程,在发现列举情况不符合问题解时,我们这时会回溯,尝试别的路径。既然是暴力枚举,那么时间复杂度必然很高,那么我们为什么要学习回溯算法呢?因为有的题目,能够暴力枚举出来,不重不漏已经是很不容易了。
剪枝:
前面我们说到回溯算法时,已经强调了它有时间复杂度较高这一缺点,而剪枝操作正可以优化我们解题过程中的时间复杂度,具体怎么优化呢?在暴力枚举,递归的过程中,常常会形成一个树形结构,我们可以根据问题情况和已有条件来剪掉树形结构中冗余的枝条,也就是在暴力枚举时省去一些不必要的递归过程,那么我们的时间复杂度和空间复杂度也就优化了
Leetcode.77 组合
这是解题的核心思想的一个流程图:
回溯算法代码如下:
class Solution {
public:
//利用回溯算法来求解
void backtracking(vector<vector<int>>&ans,vector<int>&res,int cur,int k,int n){
//回溯终止的条件,加入k个数
if(res.size()==k){
ans.push_back(res);
return;
}
for(int i=cur;i<=n;i++)
{
res.push_back(i);
backtracking(ans,res,i+1,k,n);
//回溯算法的核心,退一步重新选择
res.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int>>ans;//记录最后的答案并返回
vector<int>res;//记录不同的组合
backtracking(ans,res,1,k,n);
return ans;
}
};
剪枝:在我们进行某一轮取数时,如果还能取的数小于k个,那么这次取数过程一定不能形成一个组合,这时我们不再进行取数,而是跳过。举个比较简单的例子,如果还需要你4个数一个组合,而此时你只有3,4,5这三个数可选,那么再进行枚举取数是不是毫无意义呢?由具体到一般的,记录组合的数组的长度是我们已取的数的数量,那么k-res.size是我们还需要取的数的数量,如果这个数量大于n-i+1(还可以取数的数量),那么剪枝,代码如下
class Solution {
public:
//利用回溯算法来求解
void backtracking(vector<vector<int>>&ans,vector<int>&res,int cur,int k,int n){
//回溯的终止条件
if(res.size()==k){
ans.push_back(res);
return;
}
if(cur>n){
return;
}
//进行剪枝操作,此时i<=n-(k-res.size())+1
for(int i=cur;i<=n-(k-res.size())+1;i++)
{
res.push_back(i);
backtracking(ans,res,i+1,k,n);
res.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int>>ans;
vector<int>res;
backtracking(ans,res,1,k,n);
return ans;
}
};
Leetcode.216 组合总和III
回溯算法的代码如下:
class Solution {
public:
//最终返回的答案
vector<vector<int>>res;
//保存一组符合的组合
vector<int>ans;
void backtraking(int k,int n,int index){
//回溯的终止条件:保存k个数时回溯,如果相加之和为n保存进答案数组
if(ans.size()==k){
if(n==0) res.push_back(ans);
return;
}
for(int i=index;i<=9;i++){
ans.push_back(i);
n-=i;
backtraking(k,n,i+1);
//回溯算法的核心
n+=i;
ans.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
//使用数字1—9,所以初始传入1
backtraking(k,n,1);
return res;
}
};
剪枝操作的核心思想和上一题的大致相同,我们需要保证剩下的数的数量要大于等于我们还需要取的数的数量,否则直接跳过,也就是进行剪枝
class Solution {
public:
//最终返回的答案
vector<vector<int>>res;
//保存一组符合的组合
vector<int>ans;
void backtraking(int k,int n,int index){
//回溯的终止条件:保存k个数时回溯,如果相加之和为n保存进答案数组
if(ans.size()==k){
if(n==0) res.push_back(ans);
return;
}
//进行剪枝操作,保证剩余的数(9-i+1)>=我们还需要取的数(k-ans.size())
for(int i=index;i<=10-k+ans.size();i++){
ans.push_back(i);
n-=i;
backtraking(k,n,i+1);
//回溯算法的核心
n+=i;
ans.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
//使用数字1—9,所以初始传入1
backtraking(k,n,1);
return res;
}
};
Leetcode.17 电话号码的字母组合
对于本题,其本质仍然是一个简单的组合问题,可以用回溯算法来解决,但是它又绕了一个弯,需要我们通过数字获取对应的字符串,再进行组合
回溯算法的代码如下:
class Solution {
public:
//建立一个映射表,通过索引获取相应的字符串
vector<string>numToString{"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
//储存最终答案的数组
vector<string>res;
//储存每一组符合的字符串
string combination;
void backtraking(string digits,int index){
//回溯的终止条件
if(index==digits.size()){
res.push_back(combination);
return;
}
//取出对应的字符串,数字2对应的数组索引为0,以此类推
string tmp=numToString[digits[index]-'0'-2];
for(int i=0;i<tmp.size();i++){
combination.push_back(tmp[i]);
backtraking(digits,index+1);
combination.pop_back();
}
}
vector<string> letterCombinations(string digits) {
//特殊判断:如果是空字符串,返回空数组
if(digits==""){
return res;
}
backtraking(digits,0);
return res;
}
};
Leetcode.39 组合总和
这题与以上组合题的最大不同点是可以无限重复选取数字,因此我们再进行暴力搜索时需要一直对当前索引进行重复尝试
class Solution {
public:
vector<vector<int>>res;
vector<int>combination;
void backtraking(vector<int>&candidates,int index,int target){
//回溯的终止条件
if(target<=0){
if(target==0)res.push_back(combination);
return;
}
for(int i=index;i<candidates.size();i++){
//剪枝,如果当前数已经比target大了,那么跳过
if(target<candidates[i]){
continue;
}
combination.push_back(candidates[i]);
target-=candidates[i];
//此时不再是i+1,而依然是i,对当前索引进行重复尝试
backtraking(candidates,i,target);
target+=candidates[i];
combination.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtraking(candidates,0,target);
return res;
}
};
Leetcode.40 组合总和II
这道题的集合当中是存在重复元素的,因此我们再按照以往的方法来进行回溯的话会算进去重复组合,因此对于这道题,我们需要掌握一种新的技巧:去重
去重:
去重,简单来说就是不要选取重复的元素,那么去重也分为两种:一种是树枝去重,一种是树层去重。下面是我对这两个去重的解释:树枝去重就是指在一次递归中前面选取完这一元素,后面就不可以再次选取,这更像是一种纵向去重。树层去重就是说在同一层递归的for循环中,如果前面某一相同元素被选取过,自己就不再被选取。
而本题很显然是让你进行树层上的去重,举个最简单的例子,加入有三个元素1,7,1,如果让你选取元素使总和达到9,那么1,7,1都可以选,为一种组合方案。但是如果让你选取元素使总和达到8,那么选取的1,7和7,1就重复了,我们要对这部分进行去重处理。
本题的代码如下:
class Solution {
public:
vector<vector<int>>res;
vector<int>combination;
void backtraking(vector<int>&candidates,int index,int target,vector<bool>&used){
if(target<=0){
if(target==0)res.push_back(combination);
return;
}
for(int i=index;i<candidates.size();i++){
//去重加剪枝操作
//进行树层的去重,而不用进行树枝上的去重
if(i>=1&&candidates[i]==candidates[i-1]&&!used[i-1])continue;
//因为已经排过序(升序),后面的数都大于target,因此可以直接跳过此次循环
if(target<candidates[i])break;
target-=candidates[i];
used[i]=true;
combination.push_back(candidates[i]);
backtraking(candidates,i+1,target,used);
combination.pop_back();
used[i]=false;
target+=candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool>used(candidates.size(),false);
sort(candidates.begin(),candidates.end());
backtraking(candidates,0,target,used);
return res;
}
};
Leetcode.131 分割回文串
lass Solution {
public:
vector<vector<string>>res;
vector<string>ans;
//判断该字符串是否是回文串
bool isValid(string str){
int left=0;
int right=str.size()-1;
while(left<=right){
if(str[left]!=str[right])return false;
left++;
right--;
}
return true;
}
void backtraking(string s,int index){
if(index==s.size()){
res.push_back(ans);
return;
}
for(int i=index;i<s.size();i++){
string str=s.substr(index,i-index+1);
//如果不有效就直接跳过
if(!isValid(str))continue;
ans.push_back(str);
backtraking(s,i+1);
ans.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtraking(s,0);
return res;
}
};
Leetcode.93 复原IP地址
class Solution {
public:
vector<string>res;
//判断字符串s从i到j部分是否是回文串
bool isValid(string s,int i,int j){
//i>j只有在一种可能时会发生,那就是第三个.后面不再有字符
if(i>j) return false;
//不能含有前导0
if(s[i]=='0'&&i!=j) return false;
int sum=0;
while(i<=j){
//我们的字符只能是数字
if(s[i]<'0'||s[i]>'9'){
return false;
}
sum=sum*10+(s[i]-'0');
//每个整数的范围是0到255
if(sum>255||sum<0){
return false;
}
i++;
}
return true;
}
void backtraking(string s,int index,int cn){
//第三个.后面的字符串有效,保存到结果里
if(cn==3){
if(isValid(s,index,s.size()-1))
res.push_back(s);
return;
}
for(int i=index;i<s.size();i++){
if(isValid(s,index,i)){
s.insert(s.begin()+i+1,'.');
cn++;
//因为插入了一个点,由i+1变为i+2
backtraking(s,i+2,cn);
cn--;
s.erase(s.begin()+i+1);
}
else{
break;
}
}
}
vector<string> restoreIpAddresses(string s) {
backtraking(s,0,0);
return res;
}
};
Leetcode.78 子集
class Solution {
public:
vector<vector<int>>res;
vector<int>gather;
void backtraking(vector<int>&nums,int index){
if(gather.size()!=0){
res.push_back(gather);
}
for(int i=index;i<nums.size();i++){
gather.push_back(nums[i]);
backtraking(nums,i+1);
gather.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
res.push_back({});
backtraking(nums,0);
return res;
}
};
Leetcode.90 子集II
这道题与上一道题最大的区别就是数组中可能包含重复元素,需要进行去重操作,去重的步骤我们之前提到了,仍然是进行树层上的去重
class Solution {
public:
vector<vector<int>>res;
vector<int>combination;
void backtraking(vector<int>&nums,int index,vector<bool>&used){
res.push_back(combination);
for(int i=index;i<nums.size();i++){
if(i>=1&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
combination.push_back(nums[i]);
used[i]=true;
backtraking(nums,i+1,used);
used[i]=false;
combination.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool>used(nums.size(),false);
backtraking(nums,0,used);
return res;
}
};
Leetcode.491 递增子序列
该题仍然是要进行去重,但不论是去重方法还是去重思想都与之前的题目不同,首先,该题无法再向以前一样排序,因为排序后会改变原来的序列,而本题正要求递增序列。第二点,该题的去重是去同一父节点下相同元素的重,其本质与前面并不同,该题可采用在每层设置哈希表查重
class Solution {
public:
vector<vector<int>>res;
vector<int>ans;
bool isvalid(int a){
if(!ans.size()) return true;
return a>=ans.back();
}
void backtraking(vector<int>&nums,int index){
if(ans.size()>1)
res.push_back(ans);
unordered_map<int,int>Map;
for(int i=index;i<nums.size();i++){
if(!isvalid(nums[i])||Map.count(nums[i])) continue;
Map[nums[i]]++;
ans.push_back(nums[i]);
backtraking(nums,i+1);
ans.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtraking(nums,0);
return res;
}
};
Leetcode.46 全排列
该题是排列问题,有着一个不同于以往的新的变化,那就是返回的集合的顺序,在排列问题中,我们是需要考虑顺序的
class Solution {
public:
vector<vector<int>>res;
vector<int>ans;
void backtraking(vector<int>&nums,int index,vector<bool>&used){
//当尝试的索引到达数组的边界时,遍历完成,将答案保存到res中
if(ans.size()==nums.size()){
res.push_back(ans);
return;
}
//本次是排列与组合最大的不同时每次不再从index开始,而是从0开始
for(int i=0;i<nums.size();i++){
if(used[i])continue;
ans.push_back(nums[i]);
used[i]=true;
backtraking(nums,i+1,used);
used[i]=false;
ans.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool>used(nums.size(),false);
backtraking(nums,0,used);
return res;
}
};
Leetcode.47 全排列II
class Solution {
public:
vector<vector<int>>res;
vector<int>ans;
void backtraking(vector<int>&nums,int index,vector<bool>&used){
if(ans.size()==nums.size())
{
res.push_back(ans);
return;
}
for(int i=0;i<nums.size();i++){
//去重操作
if(i>=1&&nums[i]==nums[i-1]&&!used[i-1]) continue;
//去重和查重并不冲突,再次进行查重
if(used[i]) continue;
used[i]=true;
ans.push_back(nums[i]);
backtraking(nums,i+1,used);
ans.pop_back();
used[i]=false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool>used(nums.size(),false);
sort(nums.begin(),nums.end());
backtraking(nums,0,used);
return res;
}
};
Leetcode.51 N皇后
最经典的一道回溯题目,对于这题的分析以及后续的优化思路有太多细节了,我单独写了一篇博客来分析这道题,如果有不太清楚的可以去看看
class Solution {
public:
vector<vector<string>>res;
//判断摆放的皇后是否有效
bool isValid(vector<int>&record,int i,int j){
for(int k=0;k<i;k++){
//不能同列也不能同斜线
if(record[k]==j||i-k==abs(j-record[k]))return false;
}
return true;
}
void backtraking(vector<int>&record,int i){
//棋盘上的皇后已经摆放完毕,这时候准备进行记录
if(i==record.size()){
vector<string>ans;
for(int i=0;i<record.size();i++){
string str;
for(int j=0;j<record.size();j++){
if(record[i]==j){
str.push_back('Q');
}
else{
str.push_back('.');
}
}
ans.push_back(str);
}
res.push_back(ans);
return;
}
for(int j=0;j<record.size();j++)
{
if(isValid(record,i,j)){
record[i]=j;
backtraking(record,i+1);
}
}
}
vector<vector<string>> solveNQueens(int n) {
vector<int>record(n,0);
//record[i]=j:在第i行第j列摆放一个皇后
backtraking(record,0);
return res;
}
};
该题还有一个位运算的优化,感兴趣的可以去我的那篇博客看看
Leetcode.37 解数独
class Solution {
public:
bool isValid(vector<vector<char>>& board,int i,int j,char k){
for(int m=0;m<9;m++){
//判断行上的是否重复
if(board[i][m]==k)return false;
//判断列上的是否重复
if(board[m][j]==k)return false;
}
//判断9宫格里的是否重复
for(int row=i/3*3;row<i/3*3+3;row++){
for(int col=j/3*3;col<j/3*3+3;col++){
if(board[row][col]==k)return false;
}
}
return true;
}
bool backtraking(vector<vector<char>>& board){
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
//如果第i行第j列已经有数字了,那么跳过
if(board[i][j]!='.')continue;
for(char k='1';k<='9';k++){
if(isValid(board,i,j,k)){
board[i][j]=k;
//如果找到合适的那组返回true
if(backtraking(board)){
return true;
}
//回溯撤销k,重新尝试
board[i][j]='.';
}
}
//9个数都试完了依然不行,返回false
return false;
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtraking(board);
}
};
此外还有一种思路:
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) {
back(board,0,0);
}
bool isvalid(vector<vector<char>>& board,int x,int y,int n)
{
for(int i=0;i<9;i++)
{
if(board[x][i]==n)return false;
if(board[i][y]==n)return false;
if (board[(x/3)*3 + i/3][(y/3)*3 + i%3] == n)
return false;
}
return true;
}
bool back(vector<vector<char>>& board,int x,int y)
{
if(y==9)
{
x+=1;
y=0;
}
if(x==9)return true;//这是递归终止条件,从这回溯
if(board[x][y] != '.') return back(board,x,y+1);
else
{
for(char a='1';a<='9';a++)
{
if(isvalid(board,x,y,a))
{
board[x][y]=a;
if(back(board,x,y+1))return true;
board[x][y]='.';
}
}
return false;
}
//这行代码没有意义,单纯是为了编译成功,与上个题解不同
return true;
}
};