回溯算法的使用场景和用法套路
先看第一个问题
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xv8ka1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么使用回溯?
首先要清楚,本问题就是字符的组合问题。比如输入“23”就是对abc和def的两两组合,这很容易想到用两个for循环来完成。但是,问题在于输入的长度是不定的,可能是“23”,也可能是“345”。那么这样程序该写几个for循环呢?
考虑一下我们针对输入“23”的两层for循环逻辑:
1.外层循环遍历abc,并不断选择一个字符。
2.内层循环遍历def,并不断选择字符,拼接上外层循环的字符形成一个字符串。
3.在内层循环中,将拼接好的字符串加入到结果中。
所以,我们找一下适用于n层循环的统一规律:
1.遍历第k个指定串(1 <= k <= n)如“abc”,找到一个字符加入到待定结果串中。
2.若待定结果串的长度==n。那么将其添加到结果集中。然后移除当前选择的字符,选择下一个字符开始重复1。
3.否则继续进行下一个指定串遍历。
所以,我们只需要将上述过程抽象成一个方法,然后循环调用其n次,并且保证每次调用的指定串是按顺序遍历输入得到的。
那么,重复调用统一过程的逻辑就是:递归。而对于添加一个元素,递归,移除该元素这个过程,显然应该使用栈这样的数据结构存储待定结果串。
回溯模板
所以回溯是为了解决不定次数的循环问题。回溯将循环的逻辑抽离成函数的主体,然后递归调用,进入递归时判断有没有到达结果。所以回溯问题是存在模板的:
void backTrack(原始输入集, 临时结果, 结果, [控制变量]){ //控制变量是可选的,因为有的题目要求每层的选择必须不一样,或者每层的选择有要求。
if(临时结果满足输入要求(一般是集合长度满足)){
临时结果加入到结果集;
return;
}
遍历当前层可能的选择{
当前元素加入到临时结果集;
backTrack(原始输入集, 临时结果, 结果, [控制变量]);
当前元素移出临时结果集;
}
}
程序实现
public List<String> letterCombinations(String digits) {
if(digits.length() == 0){
return new ArrayList<>();
}
Map<Character, String> map = new HashMap<>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
List<String> res = new ArrayList<>();
Deque<Character> queue = new ArrayDeque<>(); //Java doc中表示应用Deque双端队列代替Stack完成栈操作
backTrack(res, map, queue, digits, 0);
return res;
}
public void backTrack(List<String> res, Map<Character, String> map, Deque<Character> queue, String digits, int index){
if(queue.size() == digits.length()){
StringBuffer sb = new StringBuffer(); //使用StringBuffer完成拼接
for(char c : queue){
sb.append(c);
}
res.add(sb.toString());
return;
}
for(int i = index; i < digits.length(); i++){ //对于每层递归要按顺序选择一个串,所以下一层要从下一个串开始选择,因此必须设置一个index指示每层能选择的串
String s = map.get(digits.charAt(i));
for(int j = 0; j < s.length(); j++){ //每层递归选择的串中元素又有多种可能,所以还需要有一个循环
char c = s.charAt(j);
queue.offerLast(c);
backTrack(res, map, queue, digits, i + 1);
queue.pollLast();
}
}
}
第二个问题
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
有效括号组合需满足:左括号必须以正确的顺序闭合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xv33m7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
问题分析
依然是不定层的循环问题,相当于这次的字符只有'('和')',注意这里就是在说每层的可能选择有两种,且入栈的')'个数不能超过栈中'('的个数。而入栈的'('个数又不能超过n。于是可以增设两个变量分别表示栈中左括号的个数,和允许入栈的右括号个数。
程序实现
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
Deque<Character> stack = new ArrayDeque<>();
backTrack(n, 0, 0, stack, res);
return res;
}
public void backTrack(int n, int leftNums, int rightNums, Deque<Character> stack, List<String> res){
if(stack.size() == n * 2){
StringBuilder sb = new StringBuilder();
for(char c : stack){
sb.append(c);
}
res.add(sb.toString());
return;
}
//每层的选择有两种:左括号与右括号。但是选择需要进行控制。n控制左括号个数,左括号个数控制右括号个数
if(leftNums < n){
stack.offerLast('(');
backTrack(n, leftNums + 1, rightNums + 1, stack, res);
stack.pollLast();
}
if(rightNums > 0){
stack.offerLast(')');
backTrack(n, leftNums, rightNums - 1, stack, res);
stack.pollLast();
}
}
第三个问题
全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xvqup5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
问题分析
元素的全排列问题,如果我们想着怎么怎么移动元素,循环,去排列出元素的所有组合就比较复杂了。其实我们想一想结果的样子:
输入1234
输出可能是1234,1243,1324,1342,1423,1432...。我们想一下第一步列出了1234,对于第二部的1243是不是相当于回退了一步,从选择3再选择4变成了先选择4再选择3?第三不是不是又回退了一下从选择了2变成了选择3?
所以我们就有了思路
1.每次在已有的元素中循环选择加入待定的结果
2.然后再按位数把所有元素都选完,选择过程中不能有重复元素
3.一旦长度满足,就把当前结果加入到最终结果集里。
4.回退,重新执行1的循环
这不正是回溯的思路吗?
程序实现
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> stack = new ArrayDeque<>();
backTrack(nums, res, stack);
return res;
}
public void backTrack(int[] nums, List<List<Integer>> res, Deque<Integer> stack){
if(stack.size() == nums.length){
List<Integer> l = new ArrayList<>();
for(int num : stack){
l.add(num);
}
res.add(l);
return;
}
for(int i = 0; i < nums.length; i++){ //每次的选择都有nums.length种,只不过要注意,选择的元素不能在当前的临时结果中重复
if(!stack.contains(nums[i])){
stack.offerLast(nums[i]);
backTrack(nums, res, stack);
stack.pollLast();
}
}
}
总结
所以,回溯可以解决的就是不定层的循环问题。回溯算法用栈的size是否等于结果的长度来控制循环的层数。唯一需要注意的就是每层循环有多少种选择,选择有哪些限制?是每层的选择有顺序要求?还是只能按顺序在一层给定元素中选择?还是任选?搞清楚乐这个问题,套回溯模板就可以了。

浙公网安备 33010602011771号