单模版覆盖回溯法题型

单模版覆盖回溯法题型

  • 在刷LC剑指Offer的回溯法这一章时候,用同一套模版过了这章所有的题,感觉很好用,遂记录一下。
回溯法基本逻辑
  • 回溯法可以看做暴力破解的升级版,在解决问题的时候的每一步都尝试所有可能的选项,最终找出所有可行的解决方案。
  • 它每到一步都会有新的选项,知道最终状态,这就十分地类似我们遍历树时候的结构(不管是BFS还是DFS),因此我们在解此类题的时候可以采用类似DFS/BFS的算法(递归实现)解题。
回溯法解题的基本框架
  • 下面是C++版的基本框架,回溯问题最关键的就是设计backtrace方法中分枝的选择上,一定程度上的剪枝会加快算法速度

  • #include <iostream>
    #include <vector>
    using namespace ::std;
    
    class Solution {
    public:
      	// 这里可以将一些要用到的变量设为全局
      	// 此处主要用到了数组长度,和数组本身
      	int len;
        vector<int> nums;
      	// path主要用于记录回溯的路径
        vector<int> path;
      	// res主要用于记录回溯的结果,其数据结构一般和我们最终要返回的结构相同
        vector<vector<int>> res;
    		// 主体方法,subsets是79题的方法名
        vector<vector<int>> YOUFUNCTIONNAME(vector<int>& nums) {
          	// 这里用于赋值
            this->nums = nums;
            this->len = nums.size();
          	// 开始回溯
            backtrace(0);
          	// 返回结果
            return res;
        }
        void backtrace(int idx){
            // 到底了,回退
          	// 这里一般用于判断是否到达了最终状态
          	// 判断条件各异,但是大体上的逻辑一般是判断-path插入res-回退
            if(idx == len){
                res.push_back(path);
                return;
            }
          
          	// 下面这些部分是分支的选择,这里的设计则决定了算法的速度
          	// 对于某些问题,适当剪枝会加快算法的速度
          
            // 没有nums[idx]的子集
            backtrace(idx+1);
            // 有nums[idx]的子集
            path.push_back(nums[idx]);
            backtrace(idx+1);
            path.pop_back();
        }
    };
    
实战分析
剑指 Offer II 079. 所有子集
  • 题目

  • 给定一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
    
    解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
    
    示例 1:
    输入:nums = [1,2,3]
    输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
    
    示例 2:
    输入:nums = [0]
    输出:[[],[0]]
    
  • 主体思路:

    • 该题就是返回数组的所有子集,那么在backtrace中的设计就有两种:选择该元素/不选择该元素,然后根据框架将结果插入res并返回即可
  • 代码实现:

    • class Solution {
      public:
        	int n;
          vector<int> nums;
        
          vector<int> path;
          vector<vector<int>> res;
      
        	vector<vector<int>> subsets(vector<int>& nums) {
              this->nums = nums;
              this->n = nums.size();
              backtrace(0);
              return res;
          }
          void backtrace(int idx){
              // 到底了,回退
              if(idx == n){
                  res.push_back(path);
                  return;
              }
              
              // 没有nums[idx]的子集
              backtrace(idx+1);
      
              // 有nums[idx]的子集
              path.push_back(nums[idx]);
              backtrace(idx+1);
              path.pop_back();
          }
      };
      
剑指 Offer II 080. 含有 k 个元素的组合
  • 题目

  • 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
    
    示例 1:
    输入: n = 4, k = 2
    输出:
    [
      [2,4],
      [3,4],
      [2,3],
      [1,2],
      [1,3],
      [1,4],
    ]
    
    示例 2:
    输入: n = 1, k = 1
    输出: [[1]]
    
  • 主体思路:

    • 按照本题的要求,在分支选择上可以考虑数量是否满足要求(k个),如果不满足则剪枝
    • 在判断上需要注意的是,首先判断是否到最终状态,如果是则看路径中选择元素的数量是否等于k,如果是则该路径是满足要求的,那么可以加入res。
  • 代码实现:

    • class Solution
      {
      public:
          int n, k;
          vector<int> path;
          vector<vector<int>> res;
          vector<vector<int>> combine(int n, int k)
          {
      
              this->n = n;
              this->k = k;
              // 从1开始
              backtrace(1);
              return res;
          }
          void backtrace(int num)
          {
              if (num > n)
              {
                  if (path.size() == k)
                      res.push_back(path);
                  return;
              }
              // 没有num的子集
              backtrace(num + 1);
      
              // 有num的子集
              if (path.size() < k)
              {
                  path.push_back(num);
                  backtrace(num + 1);
                  path.pop_back();
              }
          }
      };
      
剑指 Offer II 081. 允许重复选择元素的组合
  • 题目

  • 给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
    
    candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。 
    对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
    
    示例 1:
    输入: candidates = [2,3,6,7], target = 7
    输出: [[7],[2,2,3]]
    
    示例 2:
    输入: candidates = [2,3,5], target = 8
    输出: [[2,2,2,2],[2,3,3],[3,5]]
    
    示例 3:
    输入: candidates = [2], target = 1
    输出: []
    
    示例 4:
    输入: candidates = [1], target = 1
    输出: [[1]]
    
    示例 5:
    输入: candidates = [1], target = 2
    输出: [[1,1]]
    
  • 主体思路:

    • 按照题目的要求,每到一个状态可以任意选择元素,只要满足和=target的要求即可,为了避免重复,这里我加了一个限制,就是一个元素可以使用多次,那么我要求在backtrace到该元素的时候一次性加完,否则就不允许加了,然后加的次数也根据当前的和的大小来定,这样可以实现较大程度上的剪枝,同时保证不漏解。
  • 代码实现:

    • class Solution
      {
      public:
          vector<int> candidates;
          int target, sum;
          vector<int> path;
          vector<vector<int>> res;
          vector<vector<int>> combinationSum(vector<int> &candidates, int target)
          {
              this->candidates = candidates;
              this->target = target;
              this->sum = 0;
              backtrace(0);
              return res;
          }
          void backtrace(int idx)
          {
              if (sum == target)
                  res.push_back(path);
              if (sum >= target || idx == candidates.size())
                  return;
              // 没有candidates[idx]的子集
              backtrace(idx + 1);
              // 有candidates[idx]的子集
              int tmpSum = sum;
              for (int j = 0; j < (target - tmpSum) / candidates[idx] + 1; j++)
              {
                  path.push_back(candidates[idx]);
                  sum += candidates[idx];
                  backtrace(idx + 1);
              }
              for (int j = 0; j < (target - tmpSum) / candidates[idx] + 1; j++)
              {
                  path.pop_back();
                  sum -= candidates[idx];
              }
          }
      };
      
剑指 Offer II 082. 含有重复元素集合的组合
  • 题目

  • 给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
    
    candidates 中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。 
    
    示例 1:
    输入: candidates = [10,1,2,7,6,1,5], target = 8,
    输出:
    [
    [1,1,6],
    [1,2,5],
    [1,7],
    [2,6]
    ]
    
    示例 2:
    输入: candidates = [2,5,2,1,2], target = 5,
    输出:
    [
    [1,2,2],
    [5]
    ]
    
  • 主体思路:

    • 本题是上一题的加强版,数组中的元素可能重复,但不能重复使用
    • 此时我们将这个问题转化一下,其实本题没有限制集合加入的顺序,那么也就是说,重复的元素可以看成一个元素用多次,只不过这个多次不是无限次,而是有限制的,因此,问题82就可以转化成81,且每个元素的使用次数是有限的
    • 要实现这个想法,首先我们需要获得一个不含重复元素的数组,同时得知道这个数组中元素出现的次数,这个可以用hashmap来实现,在构造的同时我们也可以得到了不含重复元素的数组
  • 代码实现:

    • class Solution
      {
      public:
          vector<int> candidates;
          int target, sum;
          vector<int> path;
          vector<vector<int>> res;
          map<int, int> m;
          vector<vector<int>> combinationSum2(vector<int> &candidates, int target)
          {
              this->target = target;
              this->sum = 0;
              for (auto &num : candidates)
              {
                  if (m.find(num) == m.end())
                  {
                      m[num] = 1;
                      this->candidates.push_back(num);
                  }
                  else
                      m[num]++;
              }
              backtrace(0);
              return res;
          }
      
          void backtrace(int idx)
          {
              if (sum == target)
                  res.push_back(path);
              if (sum >= target || idx == candidates.size())
                  return;
              // 没有candidates[idx]的子集
              backtrace(idx + 1);
              // 有candidates[idx]的子集
            	// 每个元素都有使用次数的限制,这个限制记录在map中
              for (int j = 0; j < m[candidates[idx]]; j++)
              {
                  path.push_back(candidates[idx]);
                  sum += candidates[idx];
                  backtrace(idx + 1);
              }
              for (int j = 0; j < m[candidates[idx]]; j++)
              {
                  path.pop_back();
                  sum -= candidates[idx];
              }
          }
      };
      
      
剑指 Offer II 083. 没有重复元素集合的全排列
  • 题目

  • 给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
    
    示例 1:
    输入:nums = [1,2,3]
    输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
    
    示例 2:
    输入:nums = [0,1]
    输出:[[0,1],[1,0]]
    
    示例 3:
    输入:nums = [1]
    输出:[[1]]
    
  • 主体思想:

    • 本题要求返回全排列,那就意味着一个数组中的数字用且只能用一次,于是需要一个used数组表示是否用过对应下标的数,当path的长度达到nums数组的长度的时候则说明到达终止状态了,这个path可以加入res了
  • 代码实现:

    • class Solution {
      public:
          
        	int len;
          vector<int> nums;
          vector<int> used;
          vector<int> path;
          vector<vector<int>> res;
      
          vector<vector<int>> permute(vector<int>& nums) {
              this->nums = nums;
              this->len = nums.size();
              vector<int> tmp(nums.size(),0);
              this->used = tmp;
              backtrace();
              return res;
          }
          void backtrace()
          {
              if (path.size()==len){
                  res.push_back(path);
                  return;
              }
              for(int i=0; i<len; i++){
                  if(used[i]==0){
                      used[i]=1;
                      path.push_back(nums[i]);
                      backtrace();
                      used[i]=0;
                      path.pop_back();
                  }
              }
          }
      };
      
剑指 Offer II 084. 含有重复元素集合的全排列
  • 题目

  • 给定一个可包含重复数字的整数集合 nums ,按任意顺序 返回它所有不重复的全排列。
    
    示例 1:
    输入:nums = [1,1,2]
    输出:
    [[1,1,2],
     [1,2,1],
     [2,1,1]]
     
    示例 2:
    输入:nums = [1,2,3]
    输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
    
  • 主体思想:

    • 该题得到的数组又是可重复的(又,是因为82也是这样),于是可以参照82的思想使用map计数即可,同时强制上次使用过的元素不准使用,用以避免重复
  • 代码实现:

    • class Solution
      {
      public:
          int len;
          vector<int> nums;
          map<int, int> m;
          vector<int> path;
          vector<vector<int>> res;
      
          vector<vector<int>> permuteUnique(vector<int> &nums)
          {
              this->len = nums.size();
              for (auto &num : nums)
              {
                  if (m.find(num) == m.end())
                  {
                      m[num] = 1;
                      this->nums.push_back(num);
                  }
                  else
                      m[num]++;
              }
              backtrace(-1);
              return res;
          }
          void backtrace(int idx)
          {
              if (path.size() == len)
              {
                  res.push_back(path);
                  return;
              }
      
              for (int i = 0; i < nums.size(); i++)
              {
                  // 上一次用的数字不准用,避免重复
                  if (i != idx)
                  {
                      int tmp = m[nums[i]];
                      for (int j = 0; j < tmp; j++)
                      {
                          m[nums[i]]--;
                          path.push_back(nums[i]);
                          backtrace(i);
                      }
                      for (int j = 0; j < tmp; j++)
                      {
                          path.pop_back();
                          m[nums[i]]++;
                      }
                  }
              }
          }
      };
      
剑指 Offer II 085. 生成匹配的括号
  • 题目

  • 正整数 n 代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
    
    示例 1:
    输入:n = 3
    输出:["((()))","(()())","(())()","()(())","()()()"]
    
    示例 2:
    输入:n = 1
    输出:["()"]
    
  • 主体思想:

    • 看到括号匹配,首先会想到这是关于栈的问题,于是需要一个stack来帮助解决问题,但由于不需要知道栈内具体元素是啥(因为本来就知道),因此就只需要记录栈中元素数量即可,因此stack就可以用一个int来实现
    • 再来说backtrace的设计,我对path进行了改动,将所有路径元素都初始化成:’(’,用以减少操作,由于这是一个栈的问题,在加入’)‘的时候需要判断栈内是否还有’(’,与此同时,判断的时候需要看栈是否被清空(stack==0)
  • 代码实现:

    • class Solution
      {
      public:
          int stack, n;
          vector<char> path;
          vector<string> res;
          vector<string> generateParenthesis(int n)
          {
              this->n = n;
              this->stack = 0;
              vector<char> tmp(2 * n, '(');
              path = tmp;
              backtrace(0);
              return res;
          }
      
          void backtrace(int idx)
          {
              if (idx == 2 * n)
              {
                  if (stack == 0)
                  {
                      string tmp;
                      // vec 转 string:https://www.cnblogs.com/programer96s/p/13089435.html
                      // string 转 vec 也是一样,vec也有assign函数
                      tmp.assign(path.begin(), path.end());
                      res.push_back(tmp);
                  }
                  return;
              }
      
              // 先( 后 )
              if (stack < n)
              {
                  stack++;
                  backtrace(idx + 1);
                  stack--;
              }
      
              // 先 )后 (
              if (stack > 0)
              {
                  path[idx] = ')';
                  stack--;
                  backtrace(idx + 1);
                  path[idx] = '(';
                  stack++;
              }
          }
      };
      
剑指 Offer II 086. 分割回文子字符串
  • 题目

  • 给定一个字符串 s ,请将 s 分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
    
    回文串 是正着读和反着读都一样的字符串。
    
    示例 1:
    输入:s = "google"
    输出:[["g","o","o","g","l","e"],["g","oo","g","l","e"],["goog","l","e"]]
    
    示例 2:
    输入:s = "aab"
    输出:[["a","a","b"],["aa","b"]]
    
    示例 3:
    输入:s = "a"
    输出:[["a"]]
    
    
  • 主体思想:

    • 这题参考了剑指 Offer II 094. 最少回文分割的解法,就是得到一个判断是s[i~j]是否是一个回文的二维数组,用以减少不必要的计算
    • 然后在backtrace的设计中就只需要找出从idx出发所有的回文串即可,由于此前已经有了判断是否是回文的二维数组,则这里的寻找就变得十分高效。
  • 代码实现:

    • class Solution
      {
      public:
          string s;
          int len;
          vector<vector<int>> isPalindrome;
          vector<string> path;
          vector<vector<string>> res;
          vector<vector<string>> partition(string s)
          {
              this->s = s;
              this->len = s.size();
              vector<vector<int>> tmp(len, vector<int>(len, 0));
              isPalindrome = tmp;
              for (int i = 0; i < len; i++)
              {
                  isPalindrome[i][i] = 1;
              }
              for (int i = 0; i < len - 1; i++)
              {
                  isPalindrome[i][i + 1] = (s[i] == s[i + 1]) ? 1 : 0;
              }
              for (int i = 2; i < len; i++)
              {
                  for (int j = 0; j + i < len; j++)
                  {
                      isPalindrome[j][j + i] = (s[j] == s[j + i] && isPalindrome[j + 1][j + i - 1]) ? 1 : 0;
                  }
              }
              backtrace(0);
              return res;
          }
          void backtrace(int idx)
          {
              if (idx == len)
              {
                  res.push_back(path);
              }
      
              for (int i = idx; i < len; i++)
              {
                  if (isPalindrome[idx][i])
                  {
                      path.push_back(s.substr(idx, i - idx + 1));
                      backtrace(i + 1);
                      path.pop_back();
                  }
              }
          }
      };
      
      
剑指 Offer II 087. 复原 IP
  • 题目

  • 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
    
    有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
    
    例如:"0.1.2.201""192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1" 是 无效 IP 地址。
    
    示例 1:
    输入:s = "25525511135"
    输出:["255.255.11.135","255.255.111.35"]
    
    示例 2:
    输入:s = "0000"
    输出:["0.0.0.0"]
    
    示例 3:
    输入:s = "1111"
    输出:["1.1.1.1"]
    
    示例 4:
    输入:s = "010010"
    输出:["0.10.0.10","0.100.1.0"]
    
    示例 5:
    输入:s = "10203040"
    输出:["10.20.30.40","102.0.30.40","10.203.0.40"]
    
    
  • 主体思想:

    • 本题的一个麻烦的地方就是0需要拎出来判断一下,如果开头就是0,那么该段的ip就必须是0了,别的部分只需要把ip转化成数字判断是否在有效范围内即可
  • 代码实现:

    • class Solution {
      public:
          int len;
          string s;
          vector<string> path;
          vector<string> res;
          vector<string> restoreIpAddresses(string s) {
              this->s = s;
              this->len = s.size();
              backtrace(0);
              return res;
          }
          void backtrace(int idx){
              if(idx==len||path.size()>4){
                  if(path.size()==4){
                      // 这里res.push_back
                      res.push_back(path[0]+'.'+path[1]+'.'+path[2]+'.'+path[3]);
                  }
                  return;
              }
      
              if(s[idx]=='0'){
                  path.push_back("0");
                  backtrace(idx+1);
                  path.pop_back();
              }else{
                  int tmp = s[idx]-'0';
                  while(tmp<=255 && idx<len){
                      path.push_back(to_string(tmp));
                      backtrace(idx+1);
                      path.pop_back();
                      idx++;
                      tmp = tmp*10 + s[idx]-'0';
      
                  }
              }
          }
      };
      
      
总结
  • 至此,剑指Offer中回溯法章的题目就搞定了,只用一套模版还是能hold住的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椰子奶糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值