回溯算法 (backtracking algorithm)

2026-09-12

回溯算法和二叉树的递归(DFS) 算法非常类似,本质上就是一种暴力穷举算法。 它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当找到正确解的时候就将其记录,直到找到所有。

注意:回溯算法跟动态规划一样,都是一种穷举的方式,但是不一样的是,动态规划一般是求最值(最优解),而回溯算法一般是求所有的可行解而非最值。

回溯算法的核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」。

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return //这也是一种回退
    
    for 选择 in 选择列表:
        做选择尝试做这个选择):前序位置

        backtrack(路径, 选择列表)

        撤销选择退回到之前状态):后序位置

下面框架更清晰些:

//state 表示问题的当前状态(或者说路径),choices 表示当前状态下可以做出的选择
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &results) 
{
  // 判断是否为解(也就是回溯终止的条件~)
  if (isSolution(state)) 
  {
      recordSolution(state, results); // 记录解 
      return;// 不再继续搜索
  }

  // 遍历所有选择
  for (Choice choice : choices) 
  {
    // (如有)剪枝:判断选择是否合法
    if (isValid(state, choice)) 
    {
        // 尝试:做出选择,更新状态 (将选择列表放入当前状态或者路径中)
        makeChoice(state, choice); 

        backtrack(state, choices, res); 
        
        // 回退:撤销选择,恢复到之前的状态 (将上面放入的选择从当前状态或者路径中拿掉)
        undoChoice(state, choice);
    }
  }
}

N 皇后问题

时间复杂度是O(N!)回溯算法中对应着For循环。空间复杂度:O(N)是递归栈,但是还有O(N2)是存放的结果占用的空间

// 本题本质上就是,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后

class Solution {
public:
    // 输入棋盘边长 n,返回所有合法的放置
    vector<vector<string>> solveNQueens(int n) {

        // 每个字符串代表一行,字符串列表代表一个棋盘
        // '.' 表示空,'Q' 表示皇后,初始化空棋盘
        vector<string> board(n, string(n, '.'));//将其初始化为空棋盘
        // 注意,对于board为n个string,每个string为n个字符

        vector<vector<string>> result;//结果(存放的所有结果)
        backtrack(board,result, 0);//一开始row为0
        return result;
    }

    // 路径:board 中小于 row 的那些行都已经成功放置了皇后
    // 选择列表:第 row 行的所有列都是放置皇后的选择
    // 结束条件:row 超过 board 的最后一行
    void backtrack(vector<string>& board, vector<vector<string>>& result, int row) 
    {
        //结束的条件
        if(row==board.size())
        {
            result.push_back(board);//将当前的结果存放
        }

        //对于当前row的每一列
        for (int col = 0; col < board[0].size(); col++)
        {
            //先判断是否合法(也就是说是否可以在board[row][col]上放置皇后)
            if (!isValid(board, row, col))
            {
                continue;//若无效,那么就是当前位置不应该方
            }
            board[row][col]='Q';// 做选择
            backtrack(board,result, row+1);//回溯算法(一行一行的放,故此下面只需要检查列不用检查行)
            board[row][col]='.';// 撤销选择

        }

    }

    // 判断是否可以在 board[row][col] 放置皇后
    //因为皇后是一行一行从上往下放的,所以左下方,右下方和正下方不用检查(还没放皇后);
    //因为一行只会放一个皇后,所以每行不用检查。也就是最后只用检查上面,左上,右上三个方向。
    bool isValid(vector<string>& board, int row, int col) 
    {

        //先检查在这一行前,当前列是否有皇后放置了(固定了列,行变,就是一列)
        for(int i=0;i<row;i++)
        {
            if (board[i][col]=='Q')//存在皇后
                return false;
        }

        // 检查右上方.右上方就是当前row-1,col+1
        for(int i=row-1, j=col+1; i>=0 && j<board.size(); i--, j++)
        {
            if (board[i][j]=='Q')//存在皇后
                return false;
        }

        //检查左上方.右上方就是当前row-1,col-1
        for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--)
        {
            if (board[i][j]=='Q')//存在皇后
                return false;
        }

        return true;//都不满足则是true

    }
};

解法跟上面是一样的,只是保存结果不一样而已~当然直接返回result.size()也可以,但是在某些测试下可能会内存超出限制,为此还是应该把返回的结果为int

class Solution {
public:
    // 输入棋盘边长 n,返回所有合法的放置
    int totalNQueens(int n) {

        // 每个字符串代表一行,字符串列表代表一个棋盘
        // '.' 表示空,'Q' 表示皇后,初始化空棋盘
        vector<string> board(n, string(n, '.'));//将其初始化为空棋盘
        // 注意,对于board为n个string,每个string为n个字符

        int result=0;//结果
        backtrack(board,result, 0);//一开始row为0
        return result;
    }

    // 路径:board 中小于 row 的那些行都已经成功放置了皇后
    // 选择列表:第 row 行的所有列都是放置皇后的选择
    // 结束条件:row 超过 board 的最后一行
    void backtrack(vector<string>& board, int& result, int row) 
    {
        //结束的条件
        if(row==board.size())
        {
            result+=1;//结果+1
        }

        //对于当前row的每一列
        for (int col = 0; col < board.size(); col++)
        {
            //先判断是否合法(也就是说是否可以在board[row][col]上放置皇后)
            if (!isValid(board, row, col))
            {
                continue;//若无效,那么就是当前位置不应该方
            }
            board[row][col]='Q';// 做选择
            backtrack(board,result, row+1);//回溯算法
            board[row][col]='.';// 撤销选择

        }

    }

    // 判断是否可以在 board[row][col] 放置皇后
    bool isValid(vector<string>& board, int row, int col) 
    {

        //先检查在这一行前,当前列是否有皇后放置了
        for(int i=0;i<row;i++)
        {
            if (board[i][col]=='Q')//存在皇后
                return false;
        }

        // 检查右上方.右上方就是当前row-1,col+1
        for(int i=row-1, j=col+1; i>=0 && j<board.size(); i--, j++)
        {
            if (board[i][j]=='Q')//存在皇后
                return false;
        }

        //检查左上方.右上方就是当前row-1,col-1
        for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--)
        {
            if (board[i][j]=='Q')//存在皇后
                return false;
        }

        return true;//都不满足则是true

    }
};

排列/组合/子集问题

1.子集(元素无重不可复选)

时间复杂度为O(N*2N),空间复杂度为O(N)

class Solution {
  public:
  
      // 定义回溯算法
      void backtrack(vector<int>& nums,  vector<int>& track, vector<vector<int>>& result, int index)
      {
          // 没有终止条件,每次应该就是把当前的track放入result中
          result.push_back(track);//放入结果
  
          for(int i=index;i<nums.size();i++)
          {
              // 前序的位置
              track.push_back(nums[i]);//放入
  
              // 回溯函数调用(这里index+1,表示不允许重复使用当前数字)
              backtrack(nums, track, result, i+1);//注意是i+1
  
              // 后序的位置
              track.pop_back();// 拿出来最后一个
          }
  
      }
  
      vector<vector<int>> subsets(vector<int>& nums) {
  
          // 注意:解集不能包含重复的子集。也就是不考虑顺序
          vector<vector<int>> result;//最终的结果
          vector<int> track;//每次结果
  
          backtrack(nums, track, result, 0);
          return result;
  
      }
  };

2.组合(元素无重不可复选)

与上一题的解题思路几乎一样

class Solution {
  public:
  
      void backtrack(const int n, const int k, vector<vector<int>>& results, vector<int>& each_result, int index)
      {
          // 结束条件,当size为k时
          if(each_result.size()==k)
          {
              results.push_back(each_result);
          }
  
          // 遍历整个数组
          for(int i=index; i<=n;i++)//注意时闭区间,包含了n
          {
              // 前序位置
              each_result.push_back(i);
  
              // 回溯
              backtrack(n,k,results,each_result,i+1);//i的下一个
  
              //后序位置
              each_result.pop_back();//插入的用完后删掉
          }
      }
  
      vector<vector<int>> combine(int n, int k) {
  
          vector<vector<int>> results;
          vector<int> each_result;
          backtrack(n,k,results,each_result,1);//index为索引到的数字,从1开始
          return results;
      }
  };

组合总和III

注意要从1开始

class Solution {
    public:
        void backtrack(const int num, int target, vector<int>& each_path, vector<vector<int>>& results, int index)
        {
            //递归终止条件
            if(each_path.size()==num)//达到数目了
            {
                if(target==0)//减到0了
                    results.push_back(each_path);
                return;
            }
    
            //选择列表
            for(int i=index;i<=9;i++)//从index开始,那么就避免了排列的情况(仅仅是组合)
            {
                if(target-i<0)//进行剪枝
                    continue;
    
                // 做选择
                each_path.push_back(i);
                target-=i;
    
                backtrack(num,target,each_path,results,i+1);//下次是o的下一个
    
                //撤销选择
                each_path.pop_back();
                target+=i;
                
            }
        }
        vector<vector<int>> combinationSum3(int k, int n) {
    
            //首先由于每个数最多使用一次,因此是无重不可复选
    
            vector<int> each_path;
            vector<vector<int>> results;
            backtrack(k,n,each_path,results,1);//注意从1开始
    
            return results;        
        }
    };

分割回文串

本质上为子集问题。可重不可复选,只是额外需要判断是否回文子串(用于剪纸)

class Solution {
    public:
        //双指针法判断当前字符串是否为回文子串
        bool function(string s, int left, int right)
        {   
    
            //确保不越界以及相等
            while(left<right)
            {
                if(s[left]!=s[right])
                    return false;//返回不是回文串
                left++;
                right--;
            }
            //注意没有两者相等的情况,这样可以处理aba以及aa所有情况
            
            return true;
        }
    
        void backtrack(string s, vector<string> each_path, vector<vector<string>>& results, int index)
        {
            //递归终止条件
            if(index==s.size())
            {
                results.push_back(each_path);
                return;
            }
    
            //选择列表
            for(int i=index;i<s.size();i++)//子集形式
            {
                //先判断是否回文子串(相当于剪枝)
                if(function(s,index, i))//注意:i为当前右指针的位置
                {
                    // 若是回文子串
                    int left=index;
                    int right=i;
    
                    string cur_str=s.substr(left,right-left+1);
    
                    //做选择
                    each_path.push_back(cur_str);
    
                    //回溯调用
                    backtrack(s,each_path,results,i+1);//下次用i+1开始
    
                    // 撤销选择
                    each_path.pop_back();
    
                }
                else
                    continue;//当前跳过
            }
        }
    
        vector<vector<string>> partition(string s) {
            
            //相当于可重不可复选
            //且为子集
            vector<vector<string>> results;
            vector<string> each_path;
    
            backtrack(s,each_path,results,0);//从第一个字符开始
    
            return results;        
        }
    };

复原IP地址

跟上一题分割回文子串很像,只是额外多了要添加点的操作。

关键点在于额外构建函数每次判断字符是否合法的一位。合法就放入,然后继续回溯计算。下一次重新测[index,i]区间

class Solution {
    public:
        //额外定义一个函数判断某个字符是否合法的ip 
        bool isvail(string s, int start, int end)
        {
            if(start>end)
                return false;
            if(end-start+1>4)//长度超过了4
                return false;
            
            if(s[start]=='0' && start!=end)//也就是不是一个0,存在前导0
                return false;
            
            string cur_str=s.substr(start,end-start+1);//注意是左右闭区间,因此要+1
            int str_int=stoi(cur_str);
            if(str_int>255)//超过255
                return false;
    
            return true;//通过所有测试,为对
    
        }
        void backtrack(string s, vector<string> each_path,vector<string>& results, int index)
        {
            //递归终止的条件
            if(index==s.size())
            {
                if(each_path.size()==4)//刚刚好为4
                {
                    string cur_str;//插入点
                    for(int i=0;i<each_path.size();i++)
                    {
                        cur_str+=each_path[i]+'.';
                    }
                    results.push_back(cur_str.substr(0,cur_str.size()-1));//最后一个点不要
                }
                return;
            }
    
            //选择列表
            for(int i=index;i<s.size();i++)
            {
               if(isvail(s, index,i))//先判断这个区间是否有效
               { //有效才处理
                    //做选择
                    each_path.push_back(s.substr(index,i-index+1));
    
                    //回溯
                    backtrack(s, each_path, results,i+1);
    
                    //撤销选择
                    each_path.pop_back();
    
               }
            }
    
    
        }
        vector<string> restoreIpAddresses(string s) {
    
            // 首先只包含数字
            // 不能 重新排序或删除 s 中的任何数字
            // 因此相当于子集
            // 可重不可复选
    
            vector<string> each_path;
            vector<string> results;
            backtrack(s, each_path, results,0);
    
            return results;        
        }
    };

3.排列(元素无重不可复选)

注意:此题的全排列问题不包含重复的数字,下一题则是包含重复数字。递归的时间复杂度为O(N!)加上递归中每个有for循环,故此应该是O(N*N!)。空间复杂度应该是O(N)

class Solution {
  public:
      void calculate(vector<int>& num, int index, vector<vector<int>> &results)
      {
          //遍历完了,退出
          if(index==num.size()-1) //当index等于最后一个字节时,把对调后的num放入结果中(是否减1结果一样~)
          {
              results.push_back(num);
              return;
          }
  
          // index可以理解为一直递增,避免重复使用
          for(int i=index;i<num.size();i++)
          {
              //做选择
              swap(num[i],num[index]);//变换num中两个索引的顺序,只是采用swap代替了path数组,少了一个中间变量

              calculate(num, index+1, results);//下一次计算,注意是index+1。

              //撤销选择
              swap(num[index],num[i]);//变回去(变完后要变回去,才可以确保下次变是正确的)
          }
          
      }
  
      vector<vector<int>> permute(vector<int>& nums) {
  
          //采用递归的方法时间复杂度为O(N!)
          vector<vector<int>> results;
          calculate(nums,0, results);
          return results;
      }
  };

下面代码跟上面的区别只是上面通过索引index以及swap来保证交互的两个不会重复被用,此处用vector来记录是否被用过。本质上是一样的。以及递归空间复杂度也是O(N)所以并没有差别~

class Solution {
  public:
  
      void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& path,vector<vector<int>>& results)
      {
          //回溯回调的终止条件
          if(path.size()==nums.size())//全部用完了
          {
              results.push_back(path);
              return;
          }
  
          for(int i=0;i<nums.size();i++)//从列表中选择
          {
              //根据是否用过来剪枝
              if(used[i]==true)
                  continue;//若当前已经被用过了。就剪枝
  
              //前序位置做选择
              path.push_back(nums[i]);
              used[i]=true;//标记用过
  
              backtrack(nums, used, path,results);
  
              //后序位置撤销选择
              path.pop_back();
              used[i]=false;//撤销标记
          }
      }
  
      vector<vector<int>> permute(vector<int>& nums) {
  
          vector<bool> used(nums.size(),false);//用于标记当前元素是否被使用过
          vector<vector<int>> results;//记录所有的结果
          vector<int> path;//记录每次的结果
  
          backtrack(nums, used, path,results);
          return results;
  
      }
  };

4.子集(元素可重不可复选)

通过新增的if判据,来实现元素虽然存在重复,但是最终结果不能管顺序。空间复杂度:O(n)。临时数组 t 的空间代价是 O(n),递归时栈空间的代价为 O(n)。 而时间复杂度。首先sort排序的时间复杂度为:O(N)回溯的时间复杂度为O(2N),然后遍历每个字符,所以应该是O(N2N)至于前面排序sort的时间复杂度也是O(N),因此总的时间复杂度为O(N2N

class Solution {
  public:
  
      void backtrack(vector<int>& nums, vector<vector<int>>& results, vector<int>& each_result, int index)
      {
          //没有结束条件,每次直接返回
          results.push_back(each_result);
  
          //遍历每个字符
          for(int i=index;i<nums.size();i++)
          {   
              // 注意要对i>index才执行 ( 剪枝逻辑,值相同的相邻树枝,只遍历第一条)
              if(i>index && nums[i-1]==nums[i])//若当前跟上一个一样,那么就不执行
                  continue;//不执行
  
              //前序位置
              each_result.push_back(nums[i]);
  
              // 回溯前,先if判断当前结果是否出现过
              // if(find(results.begin(),results.end(),each_result)==results.end())//若找不到(这样做不行)
                  backtrack(nums, results,each_result,i+1);
  
              //后序位置,放入后删掉
              each_result.pop_back();
  
          }
      }
  
  
      vector<vector<int>> subsetsWithDup(vector<int>& nums) {
          // 元素虽然存在重复,但是最终结果不能管顺序
          sort(nums.begin(),nums.end());//排序一下
          vector<vector<int>> results;
          vector<int> each_result;
          backtrack(nums, results,each_result,0);//索引从0开始
  
          return results;
  
      }
  };

非递减子序列

相当于取有序的子集(可重,不可复选)。但是跟上一题不一样的是不能对原数组进行排序的,因此其原本的去重的方式不适用。此处改为用find来看是否存在重复的结果。

class Solution {
    public:
        void backtrack(vector<int> nums, vector<int> each_patch, vector<vector<int>>& results, int index)
        {
            //递归终止条件
            if(index==nums.size())
            {
                if(each_patch.size()>1)//必须大于1
                    if(find(results.begin(),results.end(),each_patch)==results.end())//找到了
                        results.push_back(each_patch);
                return;
            }
            if(each_patch.size()>1)//大于1已经可以放置了
            {
                if(find(results.begin(),results.end(),each_patch)==results.end())//找不到才放
                    results.push_back(each_patch);
            }
    
            //选择列表
            for(int i=index;i<nums.size();i++)
            {
                if(!each_patch.empty())//若不为空,且最后一个大于当前,就剪枝,不能放
                {
                    if(each_patch[each_patch.size()-1]>nums[i])
                        continue;
                }
    
                //做选择
                each_patch.push_back(nums[i]);
    
                backtrack(nums,each_patch,results,i+1);
    
                //撤销选择
                each_patch.pop_back();
    
            }
    
        }
        vector<vector<int>> findSubsequences(vector<int>& nums) {
            
            //相当于取有序的子集(可重,不可复选)
            //但是跟90.子集III不一样的是不能对原数组进行排序的,因此其原本的去重的方式不适用
    
            // 注意从样例中可以看到不能改变原本的数组排列顺序
            vector<vector<int>> results;
            vector<int> each_patch;
    
            backtrack(nums,each_patch,results,0);
    
            return results;
        }
    };

另外一种解法,额外维护一个数组来标记是否重复,但注意是标记每一层(宽度)而非深度!!!

class Solution {
    public:
        void backtrack(vector<int> nums, vector<int> each_patch, vector<vector<int>>& results, int index)
        {
            //递归终止条件
            if(index==nums.size())
            {
                if(each_patch.size()>1)//必须大于1
                    results.push_back(each_patch);
                return;
            }
            if(each_patch.size()>1)//大于1已经可以放置了
            {
                results.push_back(each_patch);
            }
    
            //选择列表
            unordered_set<int> used;//用于记录这个元素在本层是否用过
            for(int i=index;i<nums.size();i++)
            {
                if(used.find(nums[i])!=used.end())//被用过了
                    continue;
                    
                if(!each_patch.empty())//若不为空,且最后一个大于当前,就剪枝,不能放
                {
                    if(each_patch[each_patch.size()-1]>nums[i])
                        continue;
                }
    
                //做选择
                each_patch.push_back(nums[i]);
                used.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
    
                backtrack(nums,each_patch,results,i+1);
    
                //撤销选择
                each_patch.pop_back();
                //不需要撤销used,因为是用于记录是否重复!
            }
    
        }
        vector<vector<int>> findSubsequences(vector<int>& nums) {
            
            //相当于取有序的子集(可重,不可复选)
            //但是跟90.子集III不一样的是不能对原数组进行排序的,因此其原本的去重的方式不适用
    
            // 注意从样例中可以看到不能改变原本的数组排列顺序
            vector<vector<int>> results;
            vector<int> each_patch;
    
            backtrack(nums,each_patch,results,0);
    
            return results;
        }
    };

5.组合(元素可重不可复选)

组合跟子集问题时很类似的,故此解法基本一样~注意对于剩余target值的管理。等于0时返回,小于0时没必要继续计算,否则浪费计算时间~

class Solution {
  public:
      void backtrack(vector<int>& candidates, int target, vector<vector<int>>& results,vector<int>& each_result, int index)
      {
          // 定义结束标致,当target为0时
          if(target==0)
          {
              results.push_back(each_result);
              return;
          }
          
          // 遍历整个数组
          for(int i=index;i<candidates.size(); i++)
          {
              // 满足条件进行剪枝,以此去重
              if(i>index && candidates[i]==candidates[i-1])//若当前值等于上一个值.同时除掉最开始的index以保证每次选重复值的第一个
                  continue;
  
              if(target-candidates[i]<0)//超出了(再继续计算已经没有意义了)
                  return; //或者break
                  
              //前序
              each_result.push_back(candidates[i]);
  
              backtrack(candidates, target-candidates[i],results,each_result,i+1);//注意不要漏掉每次减candidates[i]
  
              each_result.pop_back();//后序的位置上删掉
  
          }
      }
  
      vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
          // 存在重复的数字,但不能包含重复的组合(顺序变化不算)
          
          //先进行排序,这样有利于去重
          sort(candidates.begin(),candidates.end());
  
          vector<vector<int>> results;
          vector<int> each_result;
          backtrack(candidates, target,results,each_result,0);
          
          return results;
      }
  };

6.排列(元素可重不可复选)

class Solution {
    public:
        void backtrack(vector<int>& nums, vector<int>& path, vector<vector<int>>& results, vector<bool>& used)
        {
            // 回溯终止的条件,当当前存满了,就放入results
            if(path.size()==nums.size())
            {
                results.push_back(path);
                return;
            }
    
            for(int i=0;i<nums.size();i++)
            {
                if(used[i]==true)
                    continue;//用过了
                
                if(i>0 && nums[i]==nums[i-1] && used[i-1]==false)//跟上一个相同,那么结果会一样,也剪枝。注意额外的约束是used[i-1]==false
                    continue;// 如果前面的相邻相等元素没有用过,则跳过。如果用了就是当前轮再次添加而已~
    
                // if(i>0 && nums[i]==nums[i-1] && used[i-1]==true)//这也可以通过,只是测试更久一些,注意:上一个是否用过来决定当前是否跳过
                //     continue;
                
                //做选择
                used[i]=true;
                path.push_back(nums[i]);
    
                backtrack(nums, path,results,used);
    
                // 撤销选择
                used[i]=false;
                path.pop_back();
            }
        }
        vector<vector<int>> permuteUnique(vector<int>& nums) {
            vector<vector<int>> results;
            vector<int> path;
            vector<bool> used(nums.size(),false);
            sort(nums.begin(),nums.end());//排序。 让相同的元素靠在一起
            backtrack(nums, path,results,used);
            return results;
        }
    };

也有一种情况是只需要输出一共的排列的数的,但用results数组的size会导致内存不够,为此用int来代替~

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

void backtrack(vector<char>& group, vector<char>& path, int& results, vector<bool>& used)
{
    // 回溯的终止条件~
    if(path.size() == group.size())
    {
        results++;
        return;
    }

    // 可以选择的列表
    for(int i = 0; i < group.size(); i++)
    {
        if(used[i]==true)//判断是否被用过
        {
            continue;
        }

        // 剪枝避开重复的元素
        if(i > 0 && group[i] == group[i - 1] && used[i - 1]==false)
        {
            continue;
        }

        // 做选择
        path.push_back(group[i]);
        used[i] = true;
        
        backtrack(group, path, results, used);

        // 撤销选择
        path.pop_back();
        used[i] = false;
    }
}

int main()
{
    //输入只包含大写字母的字符串S
    string S;
    cin >> S;
    
    vector<char> group;
    for(auto c : S)
    {
        group.push_back(c);
    }
    
    sort(group.begin(),group.end());
    // 采用回溯算法
    vector<char> path;//
    //vector<vector<char>> results;
    int results=0;
    
    vector<bool> used(group.size(), false);//是否使用过

    backtrack(group, path, results, used);

    //把结果输出
    std::cout<<results;
    
    return 0;
}

此题与上面1中的第46题不同的是包含了重复的字母,因此全排列可能包含重复的序列,故此需要去重。其余的解题思路跟上面是一样的

class Solution {
  public:
  
      void calculate(vector<int>& nums, vector<vector<int>> &result, int first_index, const int len)
      {
          if(first_index==len-1)//到达了尾部
          {
              result.push_back(nums);
              return;
          }
  
          set<int> flag;//用于记录每次是否遍历过(在set中每个元素的值都唯一,因此可以判定当前这个数字有没有出现过!)
          for(int i=first_index;i<len;i++)
          {
              //满足某些情况下,跳过不处理
              if(
                  flag.find(nums[i])!=flag.end()//如果找到一样的
              ) 
                  continue;//就不处理了,跳过
              flag.insert(nums[i]);//每次插入新的
              swap(nums[i], nums[first_index]);//调换位置
              calculate(nums, result, first_index+1,len); // first_index可以理解为固定不变的位置,变的i为与其兑换进行全排列
              swap(nums[i], nums[first_index]);//换回来,继续下次
              
          }
      }
  
  
      vector<vector<int>> permuteUnique(vector<int>& nums) {
          vector<vector<int>> result;
          calculate(nums, result, 0, nums.size());
          return result;
      }
  };

7.组合(元素无重可复选)

原本采用递归的解法

class Solution {
  public:
      void calculate(vector<vector<int>>& result_group,vector<int>& candidates, int target, vector<int>& temp, int index)
      {
          if(index==candidates.size())
              return;//最后一个了,跳出
          
          if(target==0)//找到了(放入结果并退出)
          {
              result_group.push_back(temp);
              return;
          }
  
          calculate(result_group,candidates,target,temp,index+1);//遍历每一个index
  
          // 同一个index下,遍历计算(会通过上面的更新)
          if(target-candidates[index]>=0)//大于0,可以继续计算
          {
              temp.push_back(candidates[index]);
              calculate(result_group,candidates,target-candidates[index],temp,index);
              temp.pop_back();//当前计算完就删掉
          }
      }
  
      vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
          //没有重复的数组,可以重复选择
  
          vector<vector<int>> result_group;
          vector<int> temp;
          // 不同于两数之和或三数之和,数目可以是随意的。
          // 采用递归的解法,对于选择了第i位,target=target-candidates[i],继续从candidates中找,直到target=0
          calculate(result_group,candidates,target,temp,0);
          return result_group;
          
      }
  };

上面方法的思路是可以实现的,但是不成体系,为此用回溯的框架看看

class Solution {
  public:
      void backtrack(vector<vector<int>>& results,vector<int>& candidates, int target, vector<int>& each_result, int index)
      {
          
          // 定义结束条件
          if(target==0)//找到了(放入结果并退出)
          {
              results.push_back(each_result);
              return;
          }
  
          for(int i=index;i<candidates.size();i++)
          {
              if(target-candidates[i]<0)//接下来就无法算了,为此跳过这个节点
                  continue;
  
              each_result.push_back(candidates[i]);//前序
              // 由于可以多次选择,因此无需+1,每次都重新看
              backtrack(results,candidates,target-candidates[i],each_result,i);//无需i+1,那么当前的就可以被再次选择
              each_result.pop_back();//后序
          }
  
      }
  
      vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
          //没有重复的数组,可以重复选择
  
          vector<vector<int>> results;
          vector<int> each_result;
  
          backtrack(results,candidates,target,each_result,0);
          return results;
          
      }
  };

8.子集(元素无重可复选)

此类型目前还没找到例题

9.排列(元素无重可复选)

此类型目前还没找到例题

10.元素可重可复选

元素可重可复选。但既然元素可复选,那又何必存在重复元素呢? 因此可以先对元素进行去重处理,然后就是退化为无重可复选的情况了。

有限制的回溯算法:火车进站

有两种选择的回溯算法,或者说有限制的回溯算法

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;

void backtrack(vector<int>& input_group, stack<int>& path, vector<int>& each_result, vector<vector<int>>& results, int index)
{
    //若栈为空,且所有车都进站了为终止条件
    if(index==input_group.size() && path.empty())
    {
        results.push_back(each_result);//把出站的方案记录
        return;
    }

    //火车还可以进栈
    if(index<=input_group.size()-1)
    {
        //执行选择1:将火车开入栈
        path.push(input_group[index]);

        backtrack(input_group,path,each_result,results,index+1);//一辆车进入栈了,因此index要递增

        //撤销选择,就是出栈(此处不需要记录)
        path.pop();
    }

    //若有火车可以先出栈
    if(!path.empty())
    {
        //执行选择2:将火车开出栈
        each_result.push_back(path.top());//记录出栈的结果
        path.pop();//出栈

        backtrack(input_group,path,each_result,results,index);//回溯调用

        //撤销出栈的选择
        path.push(each_result.back());//把结果放回去(也就是此时each_result中最新的一个)
        each_result.pop_back();
    }
        

}

int main() {
    int N;
    cin>>N;//一共的火车数
    vector<int> input_group;

    int temp;
    for(int i=0;i<N;i++)
    {
        cin>>temp;
        input_group.push_back(temp);
    }

    //只需要输出出站的方案,且为全部,因此用回溯算法

    stack<int> path;
    vector<vector<int>> results;
    vector<int> each_result;

    backtrack(input_group,path,each_result,results,0);

    sort(results.begin(),results.end());

    for(int i=0;i<results.size();i++)
    {
        auto temp= results[i];
        for(int j=0;j<temp.size();j++)
        {
            std::cout<<temp[j]<<" ";
        }
        std::cout<<std::endl;
    }

}

有限制/有选择的回溯算法:括号生成

对于括号合法性的判断,主要是借助「栈」(详情请见博客Link),而对于括号的生成,一般都要利用回溯算法 进行暴力穷举。

关键的解题点或者说约束点在于:对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量

class Solution {
  public:
      void back(int n, int num_left, int num_right, string& path, vector<string>& result)
      {
          //回溯终止条件
          if(num_left==num_right && num_left==n)
          {
              result.push_back(path);
              return;
          }
  
          //选择1:放入左括号
          if(num_left<n)//可以放入左括号
          {
              // 选择
              path.push_back('(');
              num_left++;
  
              back(n, num_left, num_right, path, result);
              
              //撤销选择
              path.pop_back();
              num_left--;
  
          }
  
  
          // 选择2:放入右括号
          if(num_right<n)//可以放入右括号
          {
              if(num_left>num_right)//才是真正可以放入右括号。放完之后,num_left>=num_right
              {
                  path.push_back(')');
                  num_right++;
  
                  back(n, num_left, num_right, path, result);
  
                  //撤销选择
                  num_right--;
                  path.pop_back();
              }
          }
  
      }
  
      vector<string> generateParenthesis(int n) {
          // 1、一个「合法」括号组合的左括号数量一定等于右括号数量。
          // 2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量。
  
          string path;
          vector<string> results;
          int num_left=0,num_right=0;
          back(n, num_left, num_right, path, results);
  
          return results;
  
      }
  };

解数独

这个回溯算法需要带return bool的,一找到结果马上停止进一步搜索。

class Solution {
  public:
      bool vailed_function(vector<vector<char>>& board, int i, int j, char value) 
      {
          for (int n = 0; n < 9; n++) {
              // 判断行是否存在重复
              if (board[i][n] == value) return false;
              // 判断列是否存在重复
              if (board[n][j] == value) return false;
              // // 判断 3 x 3 方框是否存在重复
              // if (board[(i/3)*3 + n/3][(j/3)*3 + n%3] == value)
              //     return false;
          }
  
          // // //查看矩阵范围(以3为一个大格)
          for(int x=(i/3)*3;x<(i/3+1)*3;x++)//整除3
          {
              for(int y=(j/3)*3;y<(j/3+1)*3;y++)
              {
                  if(board[x][y]==value)
                      return false;
              }
          }
          return true;//上面都通过了就是true
      }
  
      //此回溯需要有返回值
      bool backtrack(vector<vector<char>>& board, int i, int j) 
      {
          //回溯的终止条件:到达了边缘
          if(i==board.size())//注意i=9是越界了~
          {
              return true;//填完了~
          }
  
          //按照列索引
          if(j==board[0].size())//当前列满了
          {
              return backtrack(board,i+1,0);//j回0
          }
  
          // 如果有预设数字,不用我们穷举(不要漏了)
          if (board[i][j] != '.') {
              return backtrack(board, i, j + 1);
          } 
  
          //遍历选择列表(注意只填1~9)
          for(char value='1';value<='9';value++)
          {
              //进行剪枝判断是否有误
              if(vailed_function(board,i,j,value)==false)
                  continue;//若不满足条件就跳掉
              
              //做选择
              board[i][j]=value;
  
              if(backtrack(board,i,j+1))//回溯调用
              {
                  return true;// 如果找到一个可行解,立即结束
              }
  
              //撤销选择
              board[i][j]='.';//注意恢复为"."就是撤销选择
          }
          
          // 穷举完 1~9,依然没有找到可行解,此路不通
          return false;
      }
      void solveSudoku(vector<vector<char>>& board) {
          //输入为9*9的键盘,空白格用 '.' 表示。
          // 需要在原地修改棋盘,将空白格子填上数字,得到一个可行解。
  
          backtrack(board,0,0);
      }
  };

划分为k个相等的子集

下面方法可以解决,但是计算量较大,没有办法通过所有的测试案例,即使添加了排序也不行

class Solution {
  public:
  
      bool backtrack(vector<int>& nums,vector<int>& path, int index, int target)
      {
          // 递归终止条件
          if(index==nums.size())//放完了
          {
              //path中每个数字都是target
              for(int i=0;i<path.size();i++)
              {
                  if(path[i]!=target)
                      return false;
              }
  
              return true;//通过全部检查那么就是true
          }
  
          //选择的列表,放入第几个桶
          for(int i=0;i<path.size();i++)
          {
              //进行剪枝
              if(path[i]+nums[index]>target)//已经满了
                  continue;
              
              //才可以放
              //做选择,放入第i个桶
              path[i]+=nums[index];
  
              if(backtrack(nums,path,index+1,target))//回溯开始处理下一个index+1
                  return true;//一找到马上返回true
  
              //撤销选择
              path[i]-=nums[index];
  
  
          }
          return false;//全部尝试完都不行,那就false
  
      }
  
      bool canPartitionKSubsets(vector<int>& nums, int k) {
  
          if(nums.size()<k)
              return false;//根本没法分
  
          //首先根据nums与k可以确定每个子集的和为多少
          int sum=0;
          for(int i=0;i<nums.size();i++)
          {
              sum+=nums[i];
          }
          if(sum%k!=0)//若整除不了k,那么就是不满足
              return false;
          int average=sum/k;//必然要整除,因为每个桶内都是整数
  
          vector<int> path(k,0);//每个桶的数字之和
          //由于只需要返回true与false,此处相当于没有result
          int target=average;//每个桶数字的目标值。初始化为目标值,一直递减,等于0就是结果
  
          // 添加排序的结果可以更快的触发回溯中的剪枝,进而加快
          sort(nums.begin(),nums.end(),greater<int>());//从大到小排列(把大的数放前面)
  
          return backtrack(nums,path,0,target);
      }
  };

换个思路去实现,也不行,当然可以加上index,让i从index开始,这样可以通过更多样例,但最终还是超时

class Solution {
  public:
  
      bool backtrack(vector<int>& nums, int k, int each_path, vector<bool>& used,int target)//index改为是球的索引
      {
          if(k==0)//全部桶都放满了
              return true;
          
          if(each_path==target)//当前桶放满了
          {
              each_path=0;//下个桶要置0
              // 装满了当前桶,递归穷举下一个桶的选择
              // 让下一个桶从 nums[0] 开始选数字
              return backtrack(nums,k-1,each_path,used,target);
          }
  
          for(int i=0;i<nums.size();i++)
          {
              //剪肢:若这个球被用过了,或者超过阈值
              if(used[i]==true || each_path+nums[i]>target)//当前球不能放
                  continue;
              
              //做选择
              each_path+=nums[i];
              used[i]=true;//标记这个球被用过了
  
              if( backtrack(nums,k,each_path,used,target))
              {
                  return true;//若此次是结果就马上返回
              }
  
              //撤销选择
              each_path-=nums[i];
              used[i]=false;
          }
          return false;//一轮下来也没有结果,就是false
  
      }
  
      bool canPartitionKSubsets(vector<int>& nums, int k) {
  
          if(nums.size()<k)
              return false;//根本没法分
  
          //首先根据nums与k可以确定每个子集的和为多少
          int sum=0;
          for(int i=0;i<nums.size();i++)
          {
              sum+=nums[i];
          }
          if(sum%k!=0)//若整除不了k,那么就是不满足
              return false;
          int average=sum/k;//必然要整除,因为每个桶内都是整数
  
          vector<bool> used(nums.size(),false);//用于确认这个球是否被使用过
          int each_path=0;//每个桶的结果(由于在递归中会被判断,为此必须进行初始化!)
          int target=average;//每个桶数字的目标值。
  
          return backtrack(nums,k,each_path,used,target);
      }
  };

下面就是让i从index开始,这样可以通过更多样例,但最终还是超时

class Solution {
  public:
  
      bool backtrack(vector<int>& nums, int k, int each_path, vector<bool>& used, int index, int target)//index改为是球的索引
      {
          if(k==0)//全部桶都放满了
              return true;
          
          if(each_path==target)//当前桶放满了
          {
              each_path=0;//下个桶要置0
              index=0;
              // 装满了当前桶,递归穷举下一个桶的选择
              // 让下一个桶从 nums[0] 开始选数字
              return backtrack(nums,k-1,each_path,used,index,target);
          }
  
          for(int i=index;i<nums.size();i++)
          {
              //剪肢:若这个球被用过了,或者超过阈值
              if(used[i]==true || each_path+nums[i]>target)//当前球不能放
                  continue;
              
              //做选择
              each_path+=nums[i];
              used[i]=true;//标记这个球被用过了
  
              if( backtrack(nums,k,each_path,used,i+1,target))
              {
                  return true;//若此次是结果就马上返回
              }
  
              //撤销选择
              each_path-=nums[i];
              used[i]=false;
          }
          return false;//一轮下来也没有结果,就是false
  
      }
  
      bool canPartitionKSubsets(vector<int>& nums, int k) {
  
          if(nums.size()<k)
              return false;//根本没法分
  
          //首先根据nums与k可以确定每个子集的和为多少
          int sum=0;
          for(int i=0;i<nums.size();i++)
          {
              sum+=nums[i];
          }
          if(sum%k!=0)//若整除不了k,那么就是不满足
              return false;
          int average=sum/k;//必然要整除,因为每个桶内都是整数
  
          vector<bool> used(nums.size(),false);//用于确认这个球是否被使用过
          int each_path=0;//每个桶的结果(由于在递归中会被判断,为此必须进行初始化!)
          int target=average;//每个桶数字的目标值。
  
          return backtrack(nums,k,each_path,used,0,target);
      }
  };

下面是最终的解决方法,参考别人的代码,找到了新的剪枝的策略!!!

class Solution {
  public:
  
      bool backtrack(vector<int>& nums,vector<int>& path, int index, int target)
      {
          // 递归终止条件
          if(index==nums.size())//放完了
          {
              // 其实这个地方不需要判断,因为当 index == num.length 时,所有球已经按要求装入所有桶,所以肯定是一个满足要求的解
              // //判断path中每个数字都是target
              // for(int i=0;i<path.size();i++)
              // {
              //     if(path[i]!=target)
              //         return false;
              // }
  
              return true;//通过全部检查那么就是true
          }
  
          //选择的列表,放入第几个桶
          for(int i=0;i<path.size();i++)
          {
              //剪枝2: 如果当前桶和上一个桶内的元素和相等,则跳过
              // 原因:如果元素和相等,那么 nums[index] 选择上一个桶和选择当前桶可以得到的结果是一致的
              if (i > 0 && path[i] == path[i - 1]) 
                  continue;//参考思路:https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/solutions/1/by-lfool-d9o7

              //进行剪枝
              if(path[i]+nums[index]>target)//已经满了
                  continue;
              
              //才可以放
              //做选择,放入第i个桶
              path[i]+=nums[index];
  
              if(backtrack(nums,path,index+1,target))//回溯开始处理下一个index+1
                  return true;//一找到马上返回true
  
              //撤销选择
              path[i]-=nums[index];
  
  
          }
          return false;//全部尝试完都不行,那就false
  
      }
  
      bool canPartitionKSubsets(vector<int>& nums, int k) {
  
          if(nums.size()<k)
              return false;//根本没法分
  
          //首先根据nums与k可以确定每个子集的和为多少
          int sum=0;
          for(int i=0;i<nums.size();i++)
          {
              sum+=nums[i];
          }
          if(sum%k!=0)//若整除不了k,那么就是不满足
              return false;
          int average=sum/k;//必然要整除,因为每个桶内都是整数
  
          vector<int> path(k,0);//每个桶的数字之和
          //由于只需要返回true与false,此处相当于没有result
          int target=average;//每个桶数字的目标值。初始化为目标值,一直递减,等于0就是结果
  
          // 添加排序的结果可以更快的触发回溯中的剪枝,进而加快
          sort(nums.begin(),nums.end(),greater<int>());//从大到小排列(把大的数放前面)
  
          return backtrack(nums,path,0,target);
      }
  };

这题其实跟下面牛客网的题目很像,只是牛客网的这道题要求3的倍数和5的倍数不能放一起。通过递归即可解决~

关键是分解问题的思路~

#include <iostream>
#include <vector>
using namespace std;

bool callback(int sum3, int sum5, vector<int> other, int index)
{
    //递归终止的条件
    if(index==other.size())
    {
        return sum3==sum5;//最终检查两个是否相等
    }

    //要么加到3上,要么加到5上
    return callback(sum3+other[index],sum5,other,index+1) || callback(sum3,sum5+other[index],other,index+1) ;

}

int main() {
    int N;
    cin>>N;

    //由于所有5的倍数必须在其中一个组中,所有3的倍数在另一个组中。
    // 因此可以将输入先分为三个组,
    int sum3=0;
    int sum5=0;
    vector<int> other;//其他数先放other中

    int temp;
    while(N--)
    {
        cin>>temp;
        if(temp%5==0)//5的倍数
            sum5+=temp;
        else if(temp%3==0)//3的倍数在另一个组中(不包括5的倍数)
            sum3+=temp;
        else
            other.push_back(temp);
    
    }

    if(callback(sum3,sum5,other,0))
    {
        std::cout<<"true";
    }
    else {
        std::cout<<"false";
    }
}

电话号码的数字组合

hash table与回溯算法的结合。

时间复杂度:O(3m×4n ), 其中 m 是输入中对应 3 个字母的数字个数(包括数字 2、3、4、5、6、8),n 是输入中对应 4 个字母的数字个数(包括数字 7、9),m+n 是输入数字的总个数。当输入包含 m 个对应 3 个字母的数字和 n 个对应 4 个字母的数字时,不同的字母组合一共有3m×4n种,需要遍历每一种字母组合。

空间复杂度:O(m+n),其中 m 是输入中对应 3 个字母的数字个数,n 是输入中对应 4 个字母的数字个数,m+n 是输入数字的总个数。除了返回值以外,空间复杂度主要取决于哈希表以及回溯过程中的递归调用层数,哈希表的大小与输入无关,可以看成常数,递归调用层数最大为 m+n。

class Solution {
    public:
        map<char,int> num_group=
        {
            {'a',2},        {'b',2},        {'c',2},
            {'d',3},        {'e',3},        {'f',3},
            {'g',4},        {'h',4},        {'i',4},
            {'j',5},        {'k',5},        {'l',5},
            {'m',6},        {'n',6},        {'o',6},
            {'p',7},        {'q',7},        {'r',7},        {'s',7},
            {'t',8},        {'u',8},        {'v',8},
            {'w',9},        {'x',9},        {'y',9},        {'z',9}
        };
    
        void backtrack(string digits,string path,vector<string> &result,int index)
        {
            //回溯终止条件
            if(path.size()==digits.size())
            {
                result.push_back(path);
                return;
            }
    
            for(auto it=num_group.begin();it!=num_group.end();it++)
            {
                if(it->second!=(digits[index]-'0'))//注意是字符串
                    continue;//进行剪枝
    
                //做选择
                path.push_back(it->first);//添加字符
                index=index+1;
    
                backtrack(digits,path,result,index);
    
                //撤销选择
                path.pop_back();//删掉添加的
                index=index-1;
    
            }
    
        }
    
        vector<string> letterCombinations(string digits) {          
    
            vector<string> result;//digits的尺寸与result的一样~
            string path;
            //回溯算法
            if(digits.size()!=0)//为空的情况下,会输出“”,但结果要求是空的,因此要加上这个判断
                backtrack(digits,path,result,0);
    
            return result;
    
        }
    };

目标和

看到这道题的第一思路就是采用回溯算法,跟组合和有点类似。注意终止条件应该是index==nums.size()而不是index==nums.size()-1

class Solution {
  public:
      void backtrack(vector<int>& nums, int each_path, int& results, int target, int index)
      {
          //递归终止条件
          if(index==nums.size())//超越了~
          {
              if(each_path==target)
                  results++;
              
              return;
          }
  
          //做选择
          each_path+=nums[index];
          backtrack(nums,each_path,results,target,index+1);//下一个
          //撤销选择
          each_path-=nums[index];
  
          //做选择
          each_path-=nums[index];//减法
          backtrack(nums,each_path,results,target,index+1);//下一个
          //撤销选择
          each_path+=nums[index];//加回来
  
      }
  
      int findTargetSumWays(vector<int>& nums, int target) {
  
          int each_path=0;
          int results=0;
          backtrack(nums,each_path,results,target,0);
  
          return results;
      }
  };

此题也可以用动态规划Link去求解,但是思路比较绕,还是回溯算法比较直观。

重新安排行程

注意出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。从而导致计算超时。因此需要剪枝策略检查是否前后两个的机票一模一样。其次,通过预先的排序也可以保证输出的结果字典序最小,而不是全部算完再排序~

class Solution {
    public:
        bool backtrack(vector<vector<string>> tickets,vector<string> each_path,vector<string>& results, vector<bool> used)
        {
            //递归终止条件
            if(each_path.size()==tickets.size()+1)//全部都用完了,应该为数目+1
            {
                results=each_path;
                return true;
            }
    
            //选择列表(每次都重全部tickerts中选合适的)
            for(int i=0;i<tickets.size();i++)
            {
                if(i>0 
                    && tickets[i][0]==tickets[i-1][0] && tickets[i][1] == tickets[i - 1][1]//完全一模一样的情况(那就没必要再测,跳过)
                    && used[i-1]==false)
                        continue;// 如果跟上一个一样,且上一个没用过,那就跳过,因为只是重复使用
            
                if(tickets[i][0]==each_path.back() //若当前的第一个跟数组中最后一个相等,那么就可以使用飞
                && used[i]==false //且当前机票没有被使用
                )
                {
                    //做选择
                    each_path.push_back(tickets[i][1]);//放入
                    used[i]=true;//标记当前机票已经被使用了
    
                    //回溯
                    if(backtrack(tickets,each_path,results,used))
                        return true;//找到了马上返回
    
                    //撤销选择
                    each_path.pop_back();
                    used[i]=false;
    
                }
    
            }
    
            return false;
        }
        vector<string> findItinerary(vector<vector<string>>& tickets) {
            
            //注意题目要求:所有的机票 必须都用一次 且 只能用一次。
            // 有点类似排列(无重不可复选)
    
            sort(tickets.begin(), tickets.end());//进行排序,(保证了,第一个找到的就是字典序最小的,同时也用于剪枝)
        
            vector<string> results;
            vector<string> each_path;
            each_path.push_back("JFK");//把起点push进去
            vector<bool> used(tickets.size(), false);//记录是否被用过
            backtrack(tickets,each_path,results,used);
    
            return results;
        }
    };

回溯算法解二叉树类题目

毕竟回溯算法也是递归的一种,而递归算法在二叉树类题目中非常常见,因此回溯算法也常用于解决二叉树类题目。 详细请见博客Link

总结:回溯算法与递归算法的区别

回溯算法模板的核心是在递归前做选择,递归后撤销选择。两者是很像的。 个人觉得回溯可以看出是递归的一种。

而对于二叉树中的递归(DFS) 回溯算法的关注点在「树枝」,DFS 算法的关注点在「节点」。如下代码所示。 直观理解就是回溯的前序和后序操作在for循环内,而DFS的前序和后序操作在for循环外。

// 回溯算法框架模板
void backtrack(...) {
    if (到达叶子节点) {
        return;
    }

    for (int i = 0, i < n; i++) {
        // 做选择
        ...

        backtrack(...)

        // 撤销选择
        ...
    }
}

// DFS 算法框架模板
void dfs(...) {
    if (到达叶子节点) {
        return;
    }
    // 做选择
    ...
    for (int i = 0, i < n; i++) {
        dfs(...)
    }
    // 撤销选择
    ...
}

回溯与二叉树的递归

回溯算法和 二叉树的递归(DFS) 算法的细微差别是:回溯算法是在遍历「树枝」,DFS 算法是在遍历「节点」。 通过下题来理解这个观点:

给定一棵二叉树,搜索并记录所有值为 7 的节点,请返回节点列表。 对于此题,如果直接用二叉树的递归遍历,代码如下:

  void preOrder(TreeNode *root) {
    if (root == nullptr) {
        return;
    }

    //在前序的位置上
    if (root->val == 7) { 
        // 记录解
        res.push_back(root);//记录的为节点列表
    }

    preOrder(root->left);
    preOrder(root->right);
}

在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径:

    void preOrder(TreeNode *root) 
    {
      //到达就返回
      if (root == nullptr) {
          return;
      }

      // 尝试 
      path.push_back(root);//在每次“尝试”中,通过将当前节点添加进 path 来记录路径;
      
      //记录结果及回溯调用
      if (root->val == 7) {
          // 记录解
          res.push_back(path);
      }
      preOrder(root->left); 
      preOrder(root->right); 

      // 回退 
      path.pop_back();//在“回退”前,将该节点从 path 中弹出,以恢复本次尝试之前的状态。
}

进一步地,回溯问题通常包含一个或多个约束条件,约束条件通常可用于“剪枝”。

在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的 节点。

void preOrder(TreeNode *root) 
{
  // 剪枝(若值为3,就不会放入了)
  if (root == nullptr || root->val == 3) 
  {
    return; 
  }
  
  // 尝试 
  path.push_back(root); 
  
  //不为7的时候虽然不记录结果,但是放到了path中了!!!
  if (root->val == 7) {
    // 记录解
    res.push_back(path);
  }
  preOrder(root->left); 
  preOrder(root->right); 
  
  // 回退 path.pop_back();
}

参考资料