算法通关村第十八关——回溯白银挑战笔记

该部分重点在于,如何绘制回溯图,如何根据回溯图找到回溯模板的三个关键点,厘清回溯逻辑。通过习题练习加深巩固对回溯模板的理解,顺利拿下回溯这一重难点。

回溯模板:递归出口;局部枚举;放下前任

接下来,我们就根据题目,逐题分析模板如何使用的,怎么灵活的处理模板中“局部枚举”不部分和“放下前任”部分的。

1.组合总和问题

题目见LeetCode39,描述为:给定一个无重复元素的整数数组candidates和目标证书target,找出candidates中可以使数字和为目标数target的所有不同组合,并以列表的形式返回。

题目分析:对于candidates=[2,3,6,7],target=7,我们通过画图的方式模拟回溯方式选择的过程。

通过理解上面的回溯图,我们可以清晰的发现回溯模板三个要素!

递归出口:target==0找到答案,记录path;targe<0该条路径不是答案,未找到路径

局部枚举:当前枚举的数字是candidates[i],下一次枚举的数字是candidates[i+1]

放下前任:先枚举candidates[i],然后寻找目标值为target-candidates[i]的所有路径,找寻完毕后,将candidates[i]从path数组中移除,表示该条路径已经找寻完毕,枚举下一个数字进行目标和路径寻找(相当于前任已经过去了,要重新收拾行李继续往前找寻真爱!)

需要特别注意的是,面对回溯问题,当函数的局部变量发生修改时,也需要判断该局部变量是否会影响后续枚举逻辑,因此在“放下前任”模块,也需要对关键局部变量做回溯处理!

厘清回溯模板思路后,直接上代码!

    List<List<Integer>> res = new ArrayList<>(); //记录答案
    List<Integer> path = new ArrayList<>();  //记录当前正在访问的路径

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        myDfs(candidates, 0, target);
        return res;
    }    

    public void myDfs(int[] c, int idx, int target) {
        //递归出口
        if(target < 0){
            return;
        }
        if(target == 0){
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = idx; i < c.length; i++) {
            //局部枚举
            if(c[i] <= target){
                path.add(c[i]);
                myDfs(c, i, target - c[i]);
                //放下前任
                path.remove(path.size() - 1);
            }
        }
    }

2.分割回文串

题目见LeetCode131,描述为,给定一个字符串s,将s分成一些子串,使得每个子串都是回文串,返回所有的分割方案。

题目分析:对于s="aab",我们通过画图的方式模拟回溯方式选择的过程。

通过理解上面的回溯图,我们再次梳理回溯模板三个要素!

递归出口:整个字符串都已经切割,记录切割方法;

局部枚举:当前枚举的切割方案按i个字符长度切割,下一次枚举的切割方案按i+1个字符长度切割。当前切割的子串是回文串,记录切割方式;当前切割的子串不是回文串,继续向后枚举。

放弃前任:先枚举当前的切割方案,再完成子串的回文切割任务,完成子串回文切割任务后,将当前枚举的切割方案回溯。

厘清回溯模板思路后,直接上代码!

    List<List<String>> lists = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();

    public List<List<String>> partition(String s) {
        myBackTracking(s, 0);
        return lists;
    }
    
    private void myBackTracking(String s, int startIndex) {
        //递归出口
        if(startIndex >= s.length()){
            lists.add(new ArrayList<>(deque));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            //局部枚举
            if(isPalindrome(s, startIndex, i)){
                deque.offer(s.substring(startIndex, i + 1));
            }else{
                continue;
            }
            myBackTracking(s, i + 1);
            //放下前任
            deque.pollLast();
        }
    }

3.子集问题

题目见LeetCode78,描述为:给你一个整数数组,返回该数组的幂集(所有子集)。

与组合、分割类的回溯问题不同,子集类的回溯问题需要找到所有情况,而前者仅仅需要找到满足要求的结果。我们通过下面的回溯图进一步加深对这句话的理解!

题目分析:对于s="123",我们通过画图的方式模拟回溯方式选择的过程。

来继续重复上述过程,找寻解题思路!通过理解上面的回溯图,我们再次梳理回溯模板三个要素!

递归出口:数组已经全部枚举

局部枚举:当前枚举nums[i]加入集合,下一次枚举nums[i+1]加入集合。加入集合就记录子集情况!

放弃前任:当枚举nums[i]加入集合后,需要完成nums剩余元素子集寻找问题,然后将当前nums[i]移除集合,完成回溯。

厘清回溯模板思路后,直接上代码!


    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        //空集合也是一个子集
        if (nums.length == 0) {
            result.add(new ArrayList<>());
            return result;
        }
        subsetsHelper(nums, 0);
        return result;
    }   

    private void mySubsetsHelper(int[] nums, int startIndex) {
        result.add(path);
        if(startIndex >= nums.length){
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);
            mySubsetsHelper(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }

4.排列问题

题目见LeetCode46,描述为:给定一个数组的全排列

题目分析:通过前面三道题的练习,我相信全排列的回溯图已经复现在你的脑海里了,而且我们需要练习的就是拿到陌生的问题,自动浮现回溯图,然后找到模板中三个关键问题,从而顺利解决问题。

递归出口:数组已经全部枚举,记录排列顺序

局部枚举:当前枚举剩余集合中第i个元素加入排列,下一次可以枚举剩余集合中剩余的元素加入排列(使用访问标记数组完成)

放下前任:枚举当前元素加入排列,再完成剩余集合全排列子任务,然后将当前枚举的元素移除出排列,完成回溯!

结合代码,我们再次梳理回溯模板!

    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] used;

    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0) {
            return result;
        }
        used = new boolean[nums.length];
        myPermuteHelper(nums);
        return result;
    }    

    private void myPermuteHelper(int[] nums) {
        if(path.size() == nums.length){
            result.add(new ArrayList<>(path));
        }
        for (int i = 0; i < nums.length; i++) {
            //排列中使用过nums[i]
            if(used[i]){
                continue;
            }
            path.add(nums[i]);
            used[i] = true;
            myPermuteHelper(nums);
            used[i] = false;
            path.remove(path.size() - 1);
        }
    }

5.字母大小写全排列

题目见LeetCode784,描述为:给定一个字符串s,将s中的字母转换大小写完成字母大小全排列!

题目分析:对于s="abc",我们通过画图的方式模拟回溯方式选择的过程。

来吧!继续通过理解上面的回溯图,又一次梳理回溯模板三个要素!

递归出口:枚举了全部字符

局部枚举:枚举当前字母为“变换字母”(原本是大写字母就枚举小写字母),下一次枚举原字母。

放下前任:枚举当前字母后,再完成剩余字母的全排列任务,然后将当前枚举的字母进行还原,完成回溯!

字母的大小写变换操作,可以通过异或运算完成,'a'^32=='A'

厘清回溯模板思路后,直接上代码!

    List<String> res = new ArrayList<>();

    public List<String> letterCasePermutation(String s) {
        char[] cs = s.toCharArray();
        myDfs(cs, 0);
        return res;
    }
    
    void myDfs(char[] cs, int idx) {
        res.add(String.valueOf(cs));
        for (int i = idx; i < cs.length; i++) {
            if(isDigit(cs[i])) continue;
            cs[i] = changeLetter(cs[i]);
            myDfs(cs, i + 1);
            cs[i] = changeLetter(cs[i]);
        }
    }

6.单词搜索

题目见LeetCode79,描述为:给定一个m x n的二维字符网格board和一个单词word,如果word存在于网格中,返回true;否则,返回false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”指的是水平相邻或垂直相邻。

题目分析:从上到下,从左到右遍历网格,每个坐标判断是网格字符是否是word的第k个字符,如果能找到word的第k字符,则返回true,否则返回false。

递归出口:board[i][j]!=word[k],说明该条路径搜索失败;k==word.length说明找到了字符串的结尾,找到网格中路径,可以组成字符串s。

局部枚举:枚举从当前坐标ij出发能否找到word,下一次枚举从左至右、从上至下坐标出发,能否找到word。

放弃前任:枚举当前位置ij后,再完成从ij相邻位置出发搜索word(可能是word)部分的任务,然后将ij设置为未访问,完成回溯!

我们结合代码,再次理解单词搜索的回溯过程!(有些难度!)

    public boolean myExist1(char[][] board, String word){
        int h = board.length;
        int w = board[0].length;
        boolean[][] vis = new boolean[h][w];
        //局部枚举
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                if(dfs1(board, vis, i, j, 0, word)) return true;
            }
        }
        return false;
    }

    public boolean dfs1(char[][] board, boolean[][] vis, int i, int j, int k, String s){
        int h = board.length;
        int w = board[0].length;
        //递归出口
        if(s.charAt(k) != board[i][j]) return false;
        if(k == s.length()-1) return true;

        //局部枚举
        vis[i][j] = true;
        int[][] directs={{1,0},{-1,0},{0,1},{0,-1}};
        for (int[] direct : directs) {
            int newi = i + direct[0];
            int newj = j + direct[1];
            //越界
            if(newi >= h || newj >= w || newi < 0 || newj < 0){
                continue;
            }
            if(!vis[newi][newj]){
                //从ij相邻位置出发搜索word(可能是word)部分的任务
                if(dfs1(board, vis, newi, newj, k + 1, s)){
                    return true;
                }
            }
        }
        //放弃前任
        vis[i][j] = false;
        return false;
    }

回溯确实是我的薄弱环节,请不要慌张,踏踏实实,稳步前进!朝闻道,夕死可以!

OK,《算法通关村第十八关——回溯白银挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)

再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y的陪伴!ok,拜拜,第十八关第三幕见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值