目录
【一】前言
【二】全排列
【三】电话号码的字母组合
【四】括号生成
【五】组合总和
【六】子集
【七】总结
【一】前言
回溯算法采用试错的思想,尝试分步的来解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案。
回溯算法通常与递归算法结合在一起使用,当选择了某个步骤后再递归该操作过程,最后再回溯上一步的选择(也叫做撤销选择),回溯算法里面最经典的就是全排列问题了,以下介绍全排列以及类似的回溯算法题解。
【二】全排列
【题目】:给定一个不含重复数字的数组 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]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
【题解】:这道题属于回溯里面的入门级题型,也是必须要掌握的,重点在于如何分步选择数组里面的不同元素并排序,因为相同的元素不同的排序位置都算作一个排列答案,如示例1。如何做到我们在某一分步选择了数组中某个元素后,再撤销对该元素的选择呢?这里我们需要引用一个状态标志数组,需要对之前选择过的元素进行标识以及在回溯时进行状态重置。处理好了这一关键步骤之后,其余的就是递归以及边界条件退出循环和变量的设置了。
class Solution {
public List<List<Integer>> res = new ArrayList<List<Integer>>();
public List<List<Integer>> permute(int[] nums) {
List<Integer> path = new ArrayList<Integer>();
boolean[] used = new boolean[nums.length];//状态标识数组
Integer count = 0;//代表排列到了哪层
dfs(nums,count,used,path);
return res;
}
public void dfs(int[] nums,Integer count ,boolean[] used,List<Integer> path){
if(count == nums.length){
res.add(new ArrayList<Integer>(path));
}else{
for(int i=0; i<used.length; i++){
if(!used[i]){
path.add(nums[i]);
used[i] = true;
dfs(nums,count+1,used,path);
path.remove(path.size()-1);
used[i]= false;
}
}
}
}
}
【三】电话号码的字母组合
【题目】:给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "" 输出:[]
示例 3:
输入:digits = "2" 输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i]
是范围['2', '9']
的一个数字。
【题解】:解决这个问题需要处理3个关键点:第一个是找到合适的数据结构来预处理存储电话号码上数字对应的字符组合,由于只考虑给定的2-9个数字对应的字符组合,我们很容易想到用哈希表(键值对)来存储它们的关系。第二个是找到输入数字串中某个数字对应在电话号码按键上的字符组合是哪些,这里就需要关联上第一步的哈希表了。最后一步是找到某个数字的字符串组合后,循环的拼接数字对应的字符串数组,递归和拼接以及回溯操作均在一个循环里进行。当操作的步数满足输入数字的长度时,添加拼接字符到list集合中。
class Solution {
public List<String> letterCombinations(String digits) {
List<String> combinations = new ArrayList<String>();//新建一个list集合用于存放返回字符串结果集
if (digits.length() == 0) {
return combinations;//如果字符串长度为0,则直接返回空list
}
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};//用map集合存放电话号码(2-9)对应的字符数组(a-z)
backtrack(combinations,phoneMap,digits,0,new StringBuffer());//回溯+递归算法
return combinations;
}
public void backtrack(List<String> combinations,Map<Character, String> phoneMap,String digits,int index,StringBuffer combination){
if(index == digits.length()){
combinations.add(combination.toString());//当遍历的字符位置+1等于字符的长度时,说明已经遍历完字符了,返回结果集
}else{
char digit = digits.charAt(index);//字符串中某个位置的电话号码数字
String letters = phoneMap.get(digit);//电话号码数字对应在map集合中包含有哪些字符集合
int letterCount = letters.length();//该字符集合的长度,用于遍历
for(int i=0; i<letterCount; i++){
combination.append(letters.charAt(i));//依次循环遍历存储该字符某个电话号码对应的字符
backtrack(combinations,phoneMap,digits,index+1,combination);//递归依次调用遍历电话号码字符中的数字并追加到字符集中,作为本次循环遍历的结果
combination.deleteCharAt(index);//回溯操作,删除开始加入到字符集合中遍历电话号码数字对应的字符
}
}
}
}
【四】括号生成
【题目】:数字n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。
示例 1:
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1 输出:["()"]
提示:
1 <= n <= 8
【题解】:这题有一个隐含的已知条件,我们需要挖掘出来并用到题解中,那就是拼接生成的有效括号长度为输入数字n与左右2个括号的乘积数(2n)。这个题目分为两个大的步骤考虑,第一个步骤是当拼接的字符串长度小于2n时,继续选择拼接'('或')'字符,以及递归循环和对它们的回溯操作。第二个步骤是当拼接的字符长度等于乘积2n时,需要校验拼接的字符是否满足有效括号组合,如果满足才把该拼接字符加到结果集合list中,否则不添加。
注:结合全排列的思想,先给所有拼接字符进行一个全排列,最后校验符合有效括号组合的字符串并添加到list集合中,只是这里不需要标识和重置状态,而是选择2个括号。
class Solution {
public List<String> generateParenthesis(int n) {
List<String> list = new ArrayList<String>();
Integer count = 0;
StringBuffer str = new StringBuffer("");
dfs(count,n,list,str);
return list;
}
public void dfs(Integer count,int n,List<String> list,StringBuffer str){
if(count == 2*n){
if(checkLetter(str.toString())){
list.add(str.toString());
return;
}
}
if(str.length() < 2*n){
str.append('(');
dfs(count+1,n,list,str);
str.deleteCharAt(str.length()-1);
str.append(')');
dfs(count+1,n,list,str);
str.deleteCharAt(str.length()-1);
}
}
public boolean checkLetter(String str){
char[] letter = str.toCharArray();
Deque<Character> deque = new LinkedList<Character>();
for(char ch:letter){
if(!deque.isEmpty() && ch == ')' && deque.peek() == '('){
deque.pop();//遍历字符数组,如果匹配到一组有效括号组合就弹出栈顶'('
continue;
}
deque.push(ch);
}
return deque.isEmpty()?true:false;
}
}
【五】组合总和
【题目】:给你一个 无重复元素 的整数数组candidates
和一个目标整数target
,找出candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1 输出: []
提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
【题解】:解决这题的关键是,找出无重复元素的整数数组的某一个或某几个,相加的和与目标整数相等,需要注意的是 选择的元素可以无限制重复次数的选择。所以我们可以分为2个步骤来处理,第一个是不选择之前已经选择过的元素,从而直接递归选择数组中其他不同的元素,直到某次选择的元素集合等于目标集合target时,添加到结果集合中。第二个是重复选择之前已经选择过的元素,添加到该次选择元素和与列表,并且递归和回溯操作,边界条件为操作步骤等于数组长度返回,或者某次操作选择元素和等于目标值时,把该次选择的元素集合添加到结果结合list中。
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<Integer> list = new ArrayList<Integer>();
Integer result = 0;
Integer index = 0;
dfs(index,result,list,candidates,target);
return ans;
}
public void dfs(Integer index,Integer result,List<Integer> list,int[] candidates, int target){
if(index == candidates.length){
return;
}
if(result == target){
ans.add(new ArrayList<Integer>(list));
return;
}
dfs(index+1,result,list,candidates,target);//不选择之前已经选过的元素,此时index+1
if(target - result >= 0){//可重复选择,此时index不动
result += candidates[index];
list.add(candidates[index]);
dfs(index,result,list,candidates,target);
result -= candidates[index];
list.remove(list.size()-1);
}
}
}
【六】子集
【题目】:给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
【题解】:这题与全排列可谓是刚好相反,全排列需要标识已经选择过的元素并且需要做状态重置操作,而子集不需要这些步骤,只不过这里需要对原来数组集合的子集做选择和添加,也就是说只要任何子集符合满足属于原数组(包括空集合),就可以添加到结果集合中来。只不过这里的步骤需要注意一下的是,选择了某个元素后递归操作,接下来撤销对该元素的选择后再次递归操作,从而达到再次循环往复遍历数组的效果。
class Solution {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> list = new ArrayList<Integer>();
public List<List<Integer>> subsets(int[] nums) {
dfs(0,nums);
return res;
}
public void dfs(Integer index,int[] nums){
if(index == nums.length){
res.add(new ArrayList<Integer>(list));
return;
}
list.add(nums[index]);
dfs(index+1,nums);
list.remove(list.size()-1);
dfs(index+1,nums);
}
}
【七】总结
回溯算法的核心思想是需要找到在某个步骤选择某个元素并且递归这个操作后,可以回溯上一步的操作,从而达到多个步骤最终能够实现想要的结果值。需要注意的是,要区分什么情况下需要进行状态标识和状态重置,什么情况下不需要状态标识及重置。还有选择某个元素和不选择某个元素的步骤,结合不同的场景点来使用。
我是程序大视界,坚持原创技术博客分享。
注:如果本篇博客有任何纰漏和建议,欢迎私信或评论留言!