回溯算法的使用场景和用法套路

先看第一个问题

给定一个仅包含数字 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是否等于结果的长度来控制循环的层数。唯一需要注意的就是每层循环有多少种选择,选择有哪些限制?是每层的选择有顺序要求?还是只能按顺序在一层给定元素中选择?还是任选?搞清楚乐这个问题,套回溯模板就可以了。

posted @ 2021-10-31 12:09  芝芝与梅梅  阅读(401)  评论(0)    收藏  举报