单模版覆盖回溯法题型
文章目录
- 在刷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 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 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住的。