代码随想录——回溯算法

在这里插入图片描述

组合和排列的区别:

组合不强调元素的顺序,而排列强调元素顺序

回溯法解决的问题都可以抽象为树形结构,集合的大小就构成了树的宽度,递归的深度就构成了树的深度

回溯法模板:

  • 回溯函数模板返回值及参数

  • 回溯函数终止条件

  • 回溯搜索的遍历过程

    for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

    for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

void backtracking(参数){
  if(终止条件){
    存放结果;
      return;
  }
  for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
    处理节点;
    backtracking(路径,选择列表);//递归
    回溯,撤销处理结果
  }
}

(一)组合

返回1…n中所有可能的k个数的组合(注意这里是从1开始的)

用回溯替代for循环

在这里插入图片描述

一维数组存放组合path
二维数组存放所有的组合result
void backtracking(n, k, startIndex){
  //终止条件
  if(path.size() == k){
    result.push_back(path);
    return;
  }
  //单层搜索逻辑,从startIndex开始,不能重复搜索
  for(i = startIndex; i <= n; i++){
    path.push_back(i);
    backtracking(n, k, i + 1);//按顺序搜索,取过的数不重复
    path.pop();//回溯过程
  }
}
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int n, int k, int startIndex){
        if(path.size() == k){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i <= n; i++){
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

(二)组合优化

剪枝操作:减去一些没有必要搜索的子孩子

当前层最多从n-(k-path.size)+1开始(因为是包括当前startIndex,所以要加上1)

在这里插入图片描述

void backtracking(n, k, startIndex){
  //终止条件
  if(path.size() == k){
    result.push_back(path);
    return;
  }
  //单层搜索逻辑,从startIndex开始,不能重复搜索
  //缩小i的范围,剪枝
  //for(i = startIndex; i <= n; i++){
  for(i = startIndex; i <= n - (k - path.size() + 1; i++){
    path.push_back(i);
    backtracking(n, k, i + 1);//按顺序搜索,取过的数不重复
    path.pop();//回溯过程
  }

(三)组合总和III

注意:组合不强调元素的顺序

[1,9]里面取元素

剪枝:和为n,如果当前Sum已经大于targetSum,可以直接return

k为2,当前层最多从9-(k-path.size)+1开始

path 一维数组
result 二维数组
//sum为当前累加和,targetSum为目标累加和,targetIndex为当前层从哪里开始
void backtracking(targetSum, k, Sum, targetIndex){
  //终止条件
  if(Sum > targetSum) return; //约束剪枝
  if(path.size() == k){
    if(targetSum == Sum){//目标集合
      result.push_back(path);
    }
  }
  for(int i = startIndex; i <= 9-(k - path.size) + 1; i++){
    Sum += i;
    path.push_back(i);
    backtracking(targetSum, k, Sum, i + 1);
    sum -= i; //回溯
    path.pop_back(i);
  }
}
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int targetSum, int k, int startIndex, int Sum){
        if(Sum > targetSum) return;
        if(path.size() == k && Sum == targetSum){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i ++){
            path.push_back(i);
            Sum += i;
            backtracking(targetSum, k, i + 1, Sum);
            Sum -= i;
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(n, k, 1, 0);
        return result;
    }
};

(四)电话号码的字母组合

哈希表存储映射关系

const string letterMap[10] = {
“”, // 0
“”, // 1
“abc”, // 2
“def”, // 3
“ghi”, // 4
“jkl”, // 5
“mno”, // 6
“pqrs”, // 7
“tuv”, // 8
“wxyz”, // 9
};

在这里插入图片描述

树的深度由输入的数字个数控制。每个数字对应的字母的长度对应树的宽度。

String s; //单个结果
vector<String> result;//所有结果
void backtracking(digits, index){//index指的是遍历到给定字符串遍历到第几个了,不需要startIndex了
  if(index == digits.size()){//遍历到头了,注意不要减去1
    result.push_back(s);
    return;
  }
  int digit = digit[index] - '0';//将其变为一个真正的数字
  String letter = letterMap[digit];//获取数字对应的字符串
  for(int i = 0; i < letter.size(); i ++){//这里都是从0开始
    s.push_back(letter[i]);
    backtracking(digits, index + 1); //下一层递归
    s.pop_back(letter[i]);
  }
}
class Solution {
private:
    const string letterMap[10]={
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };
    vector<string> result;
    string s;
    void backtracking(string digits, int index){
        if(index == digits.size()){
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';
        string letters = letterMap[digit];
        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) {
        //注意这种情况,如果digits为空的话,输出空vector
        if(digits.size() == 0){
            return result;
        }
        backtracking(digits, 0);
        return result;
    }
};

(五)组合总和

树的深度不确定,而是靠组合的和的情况来决定树的深度

元素可以重复使用,下一层遍历都从index开始选取,注意也不是从0开始选取,因为都从0开始选取可能和前面的重复起来。

如果和>目标和,剪枝,直接return

二维数组result;
一维数组 path;
void backtracking(candidate, target, sum, startIndex){
  if(sum > target) return;
  if(sum == target){
    result.push_back(path);
    return;
  }
  for(int i = startIndex; i < candidate.size(); i ++){
    path.push_back(candidate[i]);
    sum += candidate[i];
    backtracking(candidate, target, sum, i);//注意这里可以重复,所以不需要加1,将本层使用的元素还继续传给下一层
    sum -= candidate[i];
    path.pop_back();
  }
}
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void traversal(vector<int>& candidates, int target, int sum, int startIndex){
        if(sum > target) return;
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size(); i ++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            traversal(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        traversal(candidates, target, 0, 0);
        return result;      
    }
};

剪枝:

在for循环里面剪枝:对数组先排序,当搜索到某个分支大于target后,就不用再搜索了。

class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void traversal(vector<int>& candidates, int target, int sum, int startIndex){
        if(sum > target) return;
        if(sum == target){
            result.push_back(path);
            return;
        }
        // 如果 sum + candidates[i] > target 就终止遍历
        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            traversal(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        //先排序
        sort(candidates.begin(), candidates.end());
        traversal(candidates, target, 0, 0);
        return result;      
    }
};

(六)组合总和II

区别:集合中有重复元素,但是结果中不能有重复的组合。数组中每个数字在组合中只能使用一次。

这里的去重,是在同一层上去除已经使用过的元素。而同一个树枝上(一个结果集中),可以有相同的元素。

树层去重(√)

树枝去重

注意去重要先对数组进行排序,让相同的元素放在一起,相同的元素只要搜索一次。

path 一维
result 二维
//used数组,标记哪个元素使用过
void backtracking(nums, targetSum, Sum, startIndex, used){
  if(sum > targetSum) return;
  if(sum == targetSum){
    result.push_back(path);
    return;
  }
  //剪枝放在单层搜索逻辑中
  for(i = startIndex; i < nums.size(); i++){
    //在排序后,相同元素是紧靠在一起的
    //这里used[i - 1] == 0,是确保不在前一个的分支下面,防止树枝去重
    if(i > 0 && nums[i] == nums[i - 1]&&used[i - 1] == 0){
      continue;
    }
    path.push_back(nums[i]);
    sum += nums[i];
    used[i] = true; //维护Used数组去重
    backtracking(nums, targetSum, Sum, i + 1, used);
    used[i] = false;
    sum -= nums[i];
    path.pop_back();
  }
}
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex){
        if(sum > target) return;
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size(); i ++){
          // 树层去重,要对同一树层使用过的元素进行跳过
            if(i > startIndex && candidates[i] == candidates[i - 1]){
                continue;
            }
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, sum, i + 1);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        //注意这里要先排序
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

(七)分割回文串

类比组合问题选取元素的过程

在这里插入图片描述

一维数组path
二维数组result
//startIndex表示切割线
void backtracking(String s, startIndex){
  if(startIndex >= s.size()){
    result.push_back(path);
    return;
  }
  for(int i = startIndex; i < s.size(); i++){
    //[startIndex,i]这是当前切割的子串
    //如果是回文子串,收集结果
    if(isPartition(s, startIndex, i)){
      path.push_back(子串);
    }else{
      continue;
    }
    backtracking(s, i + 1);
    path.pop_back(); //回溯
    return;
  }
}
class Solution {
private:
   vector<string> path;
   vector<vector<string>> result;
   void backtracking(string s, int startIndex){
      if(startIndex == s.size()){
        result.push_back(path);
        return;
      }
      for(int i = startIndex; i < s.size(); i ++){
        if(isPartiton(s, startIndex, i)){
            //这是区分一般组合问题的关键,就是这里取的是字符串,用了substr方法
            string str = s.substr(startIndex, i - startIndex + 1);
            path.push_back(str);
        }else{
            continue;
        }
        backtracking(s, i + 1);
        path.pop_back();        
      }   
   }
   bool isPartiton(string str, int start, int end){
    for(int i = start,j = end; i < j; i++,j--){
        if(str[i] != str[j]){
            return false;
        }
    }
    return true;
   }
public:
    vector<vector<string>> partition(string s) {
        backtracking(s, 0);
        return result;
    }
};

(八)复原IP地址

对输入进行切割,对每段进行合法性判断

在这里插入图片描述

vector<string> result;
void backtracking(s, startIndex, pointSum){
  //pointSum为加上的逗点位置,这里是通过逗点的个数作为结束条件
  if(pointSum == 3){
    if(isvalid(s, startIndex, s.size() - 1){//左闭右闭
      result.push_back(s);
      return;
    }
  }
  for(int i = startIndex; i < s.size(); i++){
    if(isvalid(s, startIndex, i)){//左闭右闭区间,[startIndex, i]
      //插入操作
      s.insert(s.begin() + i + 1,'.');
      pointSum ++;
      //注意插入.后要从i+2开始
      backtracking(s, i + 2, pointSum);
      pointSum --;
      s.erase(s.begin() + i + 1);
    }
  }
}
class Solution {
private:
    vector<string> result;
    void backtracking(string s, int startIndex, int pointSum){
        if(pointSum == 3){
            if(isValid(s, startIndex, s.size() - 1)){//左闭右闭
                result.push_back(s);
                return;
            }
        }
        for(int i = startIndex; i < s.size(); i ++){
            if(isValid(s, startIndex, i)){
                s.insert(s.begin() + i + 1, '.');
                pointSum ++;
                backtracking(s, i + 2, pointSum);
                pointSum --;
                s.erase(s.begin() + i + 1);
            }
        }
    }
    bool isValid(string s, int startIndex, int endIndex){
        if(startIndex > endIndex){
            return false;
        }
        if(s[startIndex] == '0' && startIndex != endIndex){//只有'0'才合法,其他以0开头的非法
            return false;
        }
        int num = 0;
        for(int i = startIndex; i <= endIndex; i ++){
            if(s[i]>'9' || s[i] < '0'){//非法
                return false;
            }
            num = num*10 + (s[i] - '0');
            if(num > 255){
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        backtracking(s, 0, 0);
        return result;
    }
};

(九)子集

模板题

注意在add操作在终止判断前,因为所有节点都要加入结果集中

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(vector<int>& nums, int startIndex){
        result.push_back(path);//一开始加入的是空集
        if(startIndex >= nums.size()){
            return;
        }
        for(int i = startIndex; i < nums.size(); i ++){
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return result;
    }
};

(十)子集II

模板题,同层剪枝,注意去重要先剪枝

class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(vector<int>& nums, int startIndex){
        result.push_back(path);
        if(startIndex >= nums.size()){
            return;
        }
        for(int i = startIndex; i < nums.size(); i++){
            if(i > startIndex && nums[i] == nums[i - 1]){
                continue;
            }
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        //注意去重要先排序
        sort(nums.begin(), nums.end());
        backtracking(nums, 0);
        return result;
    }
};

(十一)递增子序列

注意这里面不可以排序,这样就改变了原数组的顺序,得到的递增子数列不是原数组的递增子序列

剪枝:树层去重,非递增树枝

unorderedset另一种去重方式

path 一维
result 二维
void backtracking(nums, startIndex){
  //子集问题可以不写终止条件,因为for循环里面遍历完了自动终止
  if(path.size() > 1){
    result.push_back(result);//这里只收个大小2以上的自己
  }
  unorderedset<int> uset;//用set记录for循环里面已经去过的数,用来树层去重
  for(int i = startIndex; i < nums.size(); i++){
    // 1. path不为空,且当前数小于path最后一个数(不满足递增),剪枝
    // 2. uset中已经有当前数(不满足集合中没有重复子集),单层去重剪枝
    if(!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()){
      continue; //注意这里不能直接break,因为后面还可能有符合的
    }
    //正常取元素的过程
    uset.insert(nums[i]);
    path.push_back(nums[i]);
    backtracking(nums, i + 1);
    path.pop_back();
    //这里uset只对当前递归有效,用于树层去重,进入下层递归后,uset重新定义了,又是一个新的集合,所以没必要回溯。和path没关系
  }
}

(十二)全排列

处理排列问题就不用使用startIndex, 但排列问题需要一个used数组,标记一条路径上已经选择的元素

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void tranvsersal(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]) continue; //如果在这条路上该元素已经使用过,则跳过他
            path.push_back(nums[i]);
            used[i] = true;
            tranvsersal(nums, used);
            used[i] = false;
            path.pop_back();
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        tranvsersal(nums, used);
        return result;
    }
};

(十三)全排列II

序列中可包含重复数字,需要同层去重(先排序,再去重)

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void transerval(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 - 1] == true,则是同一个树枝上的去重
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false){
                continue;
            }
            if(used[i] == false){
                path.push_back(nums[i]);
                used[i] = true;
                transerval(nums, used);
                used[i] = false;
                path.pop_back();
            }          
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        sort(nums.begin(), nums.end());
        transerval(nums, used);
        return result;
    }
};

(十四)重新安排行程

注意: 同一个行程可能有多张票!!!

unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets

unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets

这两个结构,我选择了后者,因为如果使用unordered_map<string, multiset<string>> targets 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。

出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。

如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。

private:
// unordered_map<出发机场,map<到达机场,航班次数>> targets
unordered_map<string, map<string, int>> targets;
// 返回值为bool,在找到第一个合适行程后,就可以返回
bool backtracking(int ticketNum, vector<string>& result){
  if(result.size() == ticketNum + 1){
    return true;
  }
  // 每轮都取result的最后一个作为开始机场,遍历其到达机场
  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){
  vector<string> result;
  for(const vector<string>& vec: tickets){
    targets[vec[0]][vec[1]] ++;
  }
  result.push_back("JFK");//起始机场
  backtracking(tickets.size(), result);
  return result;
}

(十五)N皇后

不能同行、同列,对角线

矩阵的高度为树的高度,矩阵的宽度为树的宽度

result 三维数组,包含多种合理的棋盘排布(二维)
// chessboard为棋盘,n表示棋盘大小,row为当前排到第几行
void backtracking(chessboard, n, row){
  //终止条件
  if(row == n){//遍历到最后一行
    result.push_back(chessboard);
    return;
  }
  for(int col = 0; col < n; col ++){
    // 判断在col的位置放皇后是否合法
    if(isValid(row, col, chessboard, n)){
      chessboard[row][col] = 'Q';
      backtracking(chessboard, n, row + 1);
      chessboard[row][col] = '.';
    }
  }
}
class Solution {
private:
    vector<vector<string>> result;
    void backtracking(vector<string>& chessboard, int n, int row){
        if(row == n){
            result.push_back(chessboard);
            return;
        }
        for(int col = 0; col < n; col ++){
            if(isValid(chessboard, row, col, n)){
                chessboard[row][col] = 'Q';
                backtracking(chessboard, n, row + 1);
                chessboard[row][col] = '.';
            }
        }
    }
    bool isValid(vector<string>& chessboard, int row, int col, int n){
        for(int i = 0; i < row; i++){
            if(chessboard[i][col] == 'Q'){
                return false;
            }
        }
        for(int i = 0; i < row; i++){
            for(int j = 0; j < n; j++){
                if(abs(i - row) == abs(j - col) && chessboard[i][j] == 'Q'){
                    return false;
                }
            }
        }
        return true;
    }
public:
    vector<vector<string>> solveNQueens(int n) {
      //初始化vector<string>
        vector<string> chessboard(n, std::string(n, '.'));
        backtracking(chessboard, n, 0);
        return result;
    }
};

(十六)解数独

求一个解法即可

二维递归,每个位置都要枚举可能的数值,再向下递归

//求一个数组就可以了,因此如果是true,直接返回即可
bool backtracking(board){
  //这里不需要终止条件,在下面的处理中包含了终止
  for(int i = 0; i < board.size(); i ++)
    for(int j = 0; j < board[0].size(); j ++){
      if(board[i][j] == '.'){
        for(char k = '1', k <= '9'; k ++){
          if(isValid(i, j, k, board)){
            board[i][j] = k;
            bool result = backtracking(board);
            if(result == true) return true;
            board[i][j] = '.';
          }
        }
        //如果9个数都尝试了都不行
        return false;
      }
    }
  // 每个空格都搜索了一遍,所有棋盘都填满,得到符合条件的解
  return true;
}
class Solution {
private:
    bool backtracking(vector<vector<char>>& board, int row){
        //优化了一点,每次都从当前行开始排查为‘.’的格子,但是每行对应的列还是要从0开始排查
        for(int i = row; i < 9; i ++){
            for(int j = 0; j < 9; j ++){
                if(board[i][j] == '.'){
                    for(char k = '1'; k <= '9'; k ++){
                    if(isValid(board, i, j, k)){
                        board[i][j] = k;
                        //注意传入的是当前行
                        if(backtracking(board, i)) return true;
                        board[i][j] = '.';
                    }
                }
                return false;
                }
            }
        }
        return true;
    }
    bool isValid(vector<vector<char>>& board, int row, int col, char k){
        //横向
        for(int i = 0; i < 9; i ++){
            if(board[row][i] == k) return false;
        }
        //纵向
        for(int i = 0; i < 9; i ++){
            if(board[i][col] == k) return false;
        }
        // 方格
        int startXIndex = (row/3)*3;
        int startYIndex = (col/3)*3;
        for(int i = startXIndex; i < startXIndex + 3; i ++){
            for(int j = startYIndex; j < startYIndex + 3; j ++){
                if(board[i][j] == k) return false;
            }
        }
        return true;
    }
public:
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board, 0);
    }
};
  • 13
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值