【剑指offer2】 chap13 回溯

十三、回溯法

1、知识讲解

DFS 和回溯算法区别

DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置

何时使用回溯算法

当问题需要 "回头",以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止

怎么样写回溯算法(从上而下,※代表难点,根据题目而变化)

①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※

②根据题意,确立结束条件

③找准选择列表(与函数参数相关),与第一步紧密关联※

④判断是否需要剪枝

⑤作出选择,递归调用,进入下一层

⑥撤销选择

回溯问题的类型

类型

子集、组合

子集子集 II组合组合总和组合总和 II

全排列

全排列全排列 II字符串的全排列字母大小写全排列

搜索

解数独单词搜索N皇后分割回文串二进制手表

注意:子集、组合与排列是不同性质的概念。子集、组合是无关顺序的,而排列是和元素顺序有关的,如 [1,2] 和 [2,1] 是同一个组合(子集),但 [1,2] 和 [2,1] 是两种不一样的排列!!!!因此被分为两类问题

https://leetcode-cn.com/problems/subsets/solution/c-zong-jie-liao-hui-su-wen-ti-lei-xing-dai-ni-gao-/

也可以用BFS的思路

2、题型

1.组合类问题,共有n个元素

模板(DFS)

创建结果数组+中间变量数组

构造helper实现递归(if + else)

  • 终止条件+添加中间变量数组到结果数组中(重新复制一个)
  • 递归部分:
    • 不取:  helper中的索引下移一位
    • 取+放回: 当前i加到中间变量数组中

子集问题

长度无限制,可以为空集

终止条件:index到尾   迭代继续:index 未到尾

剑指 Offer II 079. 所有子集    数组中的元素 互不相同

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        //创建结果数组
        List<List<Integer>> res = new LinkedList<>();
        LinkedList<Integer> subset = new LinkedList<>();

        helper(nums, 0, res, subset);
        return res;
    }

    private void helper(int[] nums, int i, List<List<Integer>> res, LinkedList<Integer> subset) {
        if (i == nums.length) {
            res.add(new LinkedList<>(subset));
        } else if (i < nums.length) {
            //不放
            helper(nums, i + 1, res, subset);
            //放
            subset.add(nums[i]);
            helper(nums, i + 1, res, subset);
            subset.removeLast();//放后取出来
        }
    }
}

 90. 子集 II    可能包含重复元素

法一:先排序,在递归中去重(剪枝)

法二:模仿子集I,后去重

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> temp = new LinkedList<>();
    Arrays.sort(nums);
    dfs(res, temp, 0, nums, false);

    return res;
}

private void dfs(List<List<Integer>> res, LinkedList<Integer> temp, int i, int[] nums, boolean choosePre) {
    if (i == nums.length) {
        res.add(new LinkedList<>(temp));
    } else {
        dfs(res, temp, i + 1, nums, false);
        // 剪枝
        if (i != 0 && nums[i] == nums[i-1] && !choosePre) {
            return;
        }
        temp.add(nums[i]);
        dfs(res, temp, i + 1, nums, true);
        temp.removeLast();
    }
}

784. 字母大小写全排列

public List<String> letterCasePermutation(String s) {
    List<String> res = new LinkedList<>();
    char[] chars = s.toCharArray();

    dfs(s, 0, res, chars);
    return res;
}

private void dfs(String s, int i, List<String> res, char[] chars) {
    if (i == s.length()) {
        res.add(new String(chars));
    } else {
        char ch = chars[i];
        if (Character.isLetter(ch)) {
            chars[i] = Character.toUpperCase(ch);
            dfs(s, i + 1, res, chars);

            chars[i] = Character.toLowerCase(ch);
            dfs(s, i + 1, res, chars);
        } else {
            dfs(s, i + 1, res, chars);
        }
    }
}

组合问题

长度有限制,必须==k

终止条件:长度==k     迭代继续:index 未到尾

剑指 Offer II 080. 含有 k 个元素的组合

注意及时剪枝

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> com = new LinkedList<>();
    dfs(1, n, k, com, res);
    return res;
}

void dfs(int i, int n, int k, LinkedList<Integer> com, List<List<Integer>> res) {
    if (com.size() == k) {
        res.add(new LinkedList<>(com));
    } else if (i <= n && n - i  + 1 >= k - com.size()) {
        dfs(i + 1, n, k, com, res);

        com.add(i);
        dfs(i + 1, n, k, com, res);
        com.removeLast();
    }
}

拓展:求和值有限制

选取元素可重复,i可不变         

  剑指 Offer II 081. 允许重复选择元素的组合

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new LinkedList<>();
        LinkedList<Integer> comb = new LinkedList<>();
        helper(0, target, candidates, res, comb);
        return res;
    }

    private void helper(int i, int target, int[] candidates, List<List<Integer>> res, LinkedList<Integer> comb) {
        if (target == 0) {
            res.add(new LinkedList<>(comb));
        } else if (i < candidates.length && target > 0) {
            helper(i + 1, target, candidates, res, comb);

            if (target - candidates[i] >= 0) {
                comb.add(candidates[i]);
                helper(i, target - candidates[i], candidates, res, comb);
                comb.removeLast();
            }
        }
    }
}

选取元素不可重复,i= i+1

包含重复元素,要求选取不重复,组合不重复:增加一个找下一个不重复元素的函数(或者用dp存储)

剑指 Offer II 082. 含有重复元素集合的组合 = 40. 组合总和 II

class Solution {
    private int[] dp;
    private int[] counts;

    //计数排序
    public int[] countSort(int[] arr) {
        //遍历arr
        counts = new int[51];
        for (int num : arr) {
//            if (num < 31) counts[num]++;
            counts[num]++;
        }
        //遍历counts
        int i = 0;
        int temp = 0;
        for (int num = 1; num < counts.length; num++) {
            temp += counts[num];
            while (counts[num] > 0) {
                arr[i] = num;
                dp[i] = temp;
                counts[num]--;
                i++;
            }
            
        }
        return arr;
    }

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res = new LinkedList<>();
        LinkedList<Integer> comb = new LinkedList<>();
        dp = new int[candidates.length];
        countSort(candidates);

        helper(0, target, candidates, res, comb);
        return res;
    }

    private void helper(int i, int target, int[] candidates, List<List<Integer>> res, LinkedList<Integer> comb) {
        if (target == 0) {
            res.add(new LinkedList<>(comb));
        } else if (i < candidates.length && target > 0) {
            helper(dp[i], target, candidates, res, comb);

            if (target - candidates[i] >= 0) {
                comb.add(candidates[i]);
                helper(i+1, target - candidates[i], candidates, res, comb);
                comb.removeLast();
            }
        }
    }
}

其他:分组交叉组合

17. 电话号码的字母组合  BFS

class Solution {
    public List<String> letterCombinations(String digits) {
        List<String> res = new LinkedList<>();

        if (digits.length() == 0) {
            return res;
        }

        StringBuffer temp = new StringBuffer();
        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");
        }};
        helper(digits, 0, res, temp, phoneMap);
        return res;
    }

    private void helper(String digits, int i, List<String> res, StringBuffer temp, Map<Character, String> phoneMap) {
        if (i == digits.length()) {
            res.add(temp.toString());//深拷贝还是浅拷贝
        } else if (i < digits.length()) {
            char num = digits.charAt(i);
            for (char ch : phoneMap.get(num).toCharArray()) {
                temp.append(ch);
                helper(digits, i + 1, res, temp, phoneMap);
                temp.deleteCharAt(temp.length() - 1);
            }
        }
    }
}

2.排列类问题,共有n个元素

模板(BFS)

创建结果数组

构造helper实现递归(if + else)

  • 终止条件(i==length)+构造新数组(for循环)插入到结果数组中

  • 递归部分:

    • for循环(i之后的每一个)

      • 交换 i 和 j

      • 递归helper(i+1)

      • 交换 i 和 j

无重复元素,全排列问题

剑指 Offer II 083. 没有重复元素集合的全排列  = 46. 全排列

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> pers = new LinkedList<>();

        helper(0, pers, nums);

        return pers;
    }

    private void helper(int i, List<List<Integer>> pers, int[] nums) {
        if (i == nums.length) {
            List<Integer> per = new LinkedList<>();
            for (int num : nums) {
                per.add(num);
            }
            pers.add(per);
        } else {
            for (int j = i; j < nums.length; j++) {
                swap(nums, i, j);
                helper(i + 1, pers, nums);
                swap(nums, i, j);
            }
        }
    }

    private void swap(int[] nums, int i1, int i2) {
        if (i1 != i2) {
            int temp = nums[i1];
            nums[i1] = nums[i2];
            nums[i2] = temp;
        }
    }
}

有重复元素,全排列问题

剑指 Offer II 084. 含有重复元素集合的全排列  = 47. 全排列 II

用一个哈希表存储是否交换过

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        //结果
        List<List<Integer>> res = new LinkedList<>();
        helper(nums, 0, res);
        return res;
    }

    private void helper(int[] nums, int i, List<List<Integer>> res) {
        if (i == nums.length) {
            List<Integer> temp = new LinkedList<>();
            for (int num : nums) {
                temp.add(num);
            }
            res.add(temp);
        } else {
            Set<Integer> set = new HashSet<>();
            for (int j = i; j < nums.length; j++) {
                if(!set.contains(nums[j])){
                    set.add(nums[j]);
                    
                    swap(nums, i, j);
                    helper(nums, i + 1, res);
                    swap(nums, i, j);
                }
            }
        }
    }

    private void swap(int[] nums, int i1, int i2) {
        if (i1 != i2) {
            int temp = nums[i1];
            nums[i1] = nums[i2];
            nums[i2] = temp;
        }
    }
}

3.多步骤+多选项

回溯+剪枝

剑指 Offer II 085. 生成匹配的括号

终止条件(l=r=0)

递归分支

  • l > 0
  • r:   l < r

 

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> res = new LinkedList<>();
        helper(res, n, n, "");
        return res;
    }

    private void helper(List<String> res, int l, int r, String s) {
        if (l == 0 && r == 0) {
            res.add(s);
        }
        //“(”只需要有就行
        if (l > 0) {
            helper(res, l - 1, r, s + "(");
        }
        if (l < r) {
            helper(res, l, r - 1, s + ")");
        }
    }
}

剑指 Offer II 086. 分割回文子字符串

法一:回溯+判断回文函数

class Solution {
    public List<List<String>> partition(String s) {
        //1.根据返回值类型创建结果变量
        List<List<String>> res = new LinkedList<>();
        //2.创建临时变量(可不创)
        //3.调用递归函数,写出递归方程
        helper(s, 0, res, new LinkedList<String>());
        return res;
    }

    private void helper(String s, int i, List<List<String>> res, LinkedList<String> temp) {
        //递归终止条件
        if (i == s.length()) {
            res.add(new LinkedList<>(temp));
        }
        //递归继续
        for (int j = i; j < s.length(); j++) {
            if (isPalindrome(s, i, j)) {
                temp.add(s.substring(i, j + 1));//左闭右开
                helper(s, j + 1, res, temp);
                temp.removeLast();
            }
        }
    }

    //判断是否是回文串,左闭右闭
    private boolean isPalindrome(String s, int start, int end) {
        while (start < end) {
            if (s.charAt(start++) != s.charAt(end--)) {
                return false;
            }
        }
        return true;
    }
}

 法二:回溯+预处理(中心扩展法)

class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> res = new ArrayList<>();
        int len = s.length();
        boolean[][] dp = new boolean[len][len];
        for(int i = 0; i < len; i++){
            prePro(s, i, i, dp);
            prePro(s, i, i + 1, dp);
        }
        helper(res, new ArrayList<>(), s, 0, dp);
        return res;
    }

    //进行预处理,利用中心扩展 将所有回文子串的位置存储到 dp 中
    private void prePro(String s, int left , int right, boolean[][] dp){
        while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
            dp[left][right] = true;
            left--;
            right++;
        }
    }

    private void helper(List<List<String>> res, List<String> list, String s, int index, boolean[][] dp){
        if(index == s.length()){
            res.add(new ArrayList<>(list));
            return;
        }
        for(int i = index; i < s.length(); i++){
            //利用预处理结果就不用再去判断该字符串是否是回文串
            if(!dp[index][i]){
                continue;
            }
            list.add(s.substring(index, i + 1));
            helper(res, list, s, i + 1, dp);
            list.remove(list.size() - 1);
        }
    }
}

​​​​​​​剑指 Offer II 087. 复原 IP

 

class Solution {
    public List<String> restoreIpAddresses(String s) {
        List<String> res = new LinkedList<>();
        helper(s, res, 0, 0, "", "");
        return res;
    }

    private void helper(String s, List<String> res, int i, int segI, String seg, String ip) {
        if (i == s.length() && segI == 3 && isValidSeg(seg)) {
            res.add(ip + seg);
        } else if (i < s.length() && segI <= 3) {
            char ch = s.charAt(i);
            if (isValidSeg(seg + ch)) {
                helper(s, res, i + 1, segI, seg + ch, ip);
            }

            if (seg.length() > 0 && segI < 3) {
                helper(s, res, i + 1, segI + 1, "" + ch, ip + seg + ".");
            }
        }
    }

    private boolean isValidSeg(String seg) {
        return Integer.valueOf(seg) <= 255 
                && (seg.equals("0") || seg.charAt(0) != '0');
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值