回溯法,子集选择合集

框架

回溯的核心特征,做选择。

回溯主要基于递归,它的特点是选择以及撤销选择,非常经典。子问题性质不是很明显。算法复杂度可以从子集数考虑,空间复杂度则一般为递归深度N。

回溯法框架如下:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
		剪枝
        做选择
        backtrack(路径, 选择列表)
        撤销选择

集合选择(子集数,全排列)是典型的回溯法应用,而且它们的算法非常规则,简洁。

以40,求子集组合为target的所有可能,元素不可重复,但可选集合有重复。

public class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        // 排序,方便去重
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }

    private void backtrack(int[] nums, int sum, int k, ArrayList<Integer> cur, List<List<Integer>> res) {
        // 满足条件加入集合
        if(sum == 0) res.add(new ArrayList<>(cur));
        else if (sum < 0) return;
        else {
            // 选择所有状态
            for (int i = k; i < nums.length; i++) {
                // 可选集合去重
                if (i > k && nums[i] == nums[i-1])continue;
                // 回溯
                cur.add(nums[i]);
                backtrack(nums, sum - nums[i], i + 1, cur, res);
                cur.remove(cur.size() - 1);
            }
        }
    }
}

上述算法包含了集合选择题目的所有核心要点(注释)。

  • 为何是子集数

    事实上这个算法不算直观,递归树也不算直观。但观察状态选择部分,可以发现,对于每个状态,它都有两个分支,选择(加入),或者不选,直接i++。因而本质就是子集数。

  • **全排列为何是子集数?**其实唯一的区别就是,全排列每次都考虑所有状态,而子集数选过的之前的状态不再考虑(关键是上述回溯调用的k值)。另外全排列需要标记。

  • 子集数去重,这是个很抽象的点,理解请参考 47 有重复的全排列。它的本质是,对可选集合去重,实际操作就是上述代码的sort和for中的if部分。

  • 考虑空集,请参考题目78,90.

问题

78. Subsets

子集数

https://leetcode.com/problems/subsets/

/*
* Runtime: 0 ms, faster than 100.00% of Java online submissions for Subsets.
* Memory Usage: 39.7 MB, less than 16.56% of Java online submissions for Subsets.
* */
public class SolutionV2 {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), res);
        return res;
    }

    private void backtrack(int[] nums, int k, ArrayList<Integer> cur, List<List<Integer>> res) {
        // 包括空的简洁实现方法,相当于在nums后加上null元素
        res.add(new ArrayList<>(cur));

        for (int i = k; i < nums.length; i++) {
            cur.add(nums[i]);
            backtrack(nums, i + 1, cur, res);
            cur.remove(cur.size() - 1);
        }
    }
}

90. Subsets II

子集数,集合有重复元素,求出子集不得重复

https://leetcode.com/problems/subsets-ii/

/*
Runtime: 1 ms, faster than 99.52% of Java online submissions for Subsets II.
        Memory Usage: 39.5 MB, less than 33.78% of Java online submissions for Subsets II.
*/

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        backtrack(nums, 0, new ArrayList<>(), res);
        return res;
    }

    private void backtrack(int[] nums, int k, ArrayList<Integer> cur, List<List<Integer>> res) {
        // 包括空的简洁实现方法,相当于在nums后加上null元素
        res.add(new ArrayList<>(cur));

        for (int i = k; i < nums.length; i++) {
            if (k < i && nums[i] == nums[i-1]) continue;
            cur.add(nums[i]);
            backtrack(nums, i + 1, cur, res);
            cur.remove(cur.size() - 1);
        }
    }
}

46. Permutations

全排列
https://leetcode.com/problems/permutations/

/*

Runtime: 2 ms, faster than 49.39% of Java online submissions for Permutations.
        Memory Usage: 39.1 MB, less than 82.56% of Java online submissions for Permutations.
*/

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Map<Integer, Boolean> flag = new HashMap<>();

        backtrack(nums,  new ArrayList<>(), res, flag);
        return res;
    }
    private void backtrack(int[] nums,ArrayList<Integer> cur, List<List<Integer>> res, Map<Integer, Boolean> flag) {
        if (cur.size() == nums.length) {
            res.add(new ArrayList<>(cur));
        } else {
            for (int i = 0; i < nums.length; i++) {
                if (flag.getOrDefault(i, false)) continue;
                cur.add(nums[i]);
                flag.put(i, true);
                backtrack(nums, cur, res, flag);
                cur.remove(cur.size() - 1);
                flag.put(i, false);
            }
        }
    }
}

47 Permutations II

全排列,元素有重复

https://leetcode.com/problems/permutations-ii/

/*
 * 47. Permutations II
 * 全排列
 * https://leetcode.com/problems/permutations-ii/
 * */

/*
(i > 0 && nums[i] == nums[i - 1] && !flag[i-1])该条件是去重的关键
按照类似的调节,排序后的数组重复元素优先只会选择第一个,以[1 1 2]为例子,该递归树被去重为如下,这也是在子集数去重中的原理——每次可选集合没有重复元素
         / | \
        1  1  2
       /|\
      1 1 2
     /|\
    1 1 2
>>>>>>
         / \
        1   2
       / \
      1   2
     / \
    1   2
Runtime: 1 ms, faster than 98.93% of Java online submissions for Permutations II.
Memory Usage: 39.8 MB, less than 38.95% of Java online submissions for Permutations II.
 */

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        backtrack(nums, new ArrayList<>(), res, new boolean[nums.length]);
        return res;
    }

    private void backtrack(int[] nums, ArrayList<Integer> cur, List<List<Integer>> res, boolean[] flag) {
        if (cur.size() == nums.length) {
            res.add(new ArrayList<>(cur));
        } else {
            for (int i = 0; i < nums.length; i++) {
                if (flag[i] || (i > 0 && nums[i] == nums[i - 1] && !flag[i-1])) continue;
                cur.add(nums[i]);
                flag[i] = true;
                backtrack(nums, cur, res, flag);
                cur.remove(cur.size() - 1);
                flag[i] = false;
            }
        }
    }


    public static void main(String[] args) {
        int[] candidates = {1, 1, 5};
        System.out.println(new Solution().permuteUnique(candidates));
    }
}

39. Combination Sum

组合,元素无限制,和等于目标值

https://leetcode.com/problems/combination-sum/

/*
更好的形式
Runtime: 3 ms, faster than 78.07% of Java online submissions for Combination Sum.
        Memory Usage: 39.4 MB, less than 44.99% of Java online submissions for Combination Sum.
*/
public class SolutionV2 {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> r;
        r = new ArrayList<>();
        backtrack(target, 0, new ArrayList<>(), r, candidates);
        return r;
    }

    private void backtrack(int sum, int k, ArrayList<Object> cur, List<List<Integer>> result, int[] nums) {
        if(sum == 0) result.add(new ArrayList(cur));
        else if(sum < 0) return;
        else{
            for (int i = k; i < nums.length; i++) {
                cur.add(nums[i]);
                backtrack(sum - nums[i], i, cur, result, nums);
                cur.remove(cur.size() - 1);
            }
        }
    }
}

40. Combination Sum II

组合,求目标和,元素不重复

/*
集合选择框架版本
非常好的子集数实现。
算法复杂度,它的递归树是一个倾斜的递归树,实际上不好直接考虑,一个宽松的上界为元组可重复的版本,即使递归深度为target,
每个节点能选择的为N,因而为O(target ^ N)。从子集数来考虑,该问题的状态空间等于子集数数量,也就是O(2^N).
空间复杂度,递归深度等于O(N),N为集合大小
Runtime: 3 ms, faster than 81.28% of Java online submissions for Combination Sum II.
        Memory Usage: 39.5 MB, less than 41.18% of Java online submissions for Combination Sum II.
*/

public class SolutionV2 {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }

    private void backtrack(int[] nums, int sum, int k, ArrayList<Integer> cur, List<List<Integer>> res) {
        if(sum == 0) res.add(new ArrayList<>(cur));
        else if (sum < 0) return;
        else {
            for (int i = k; i < nums.length; i++) {
                // 防止组合重复的要点
                if (i > k && nums[i] == nums[i-1])continue;
                cur.add(nums[i]);
                backtrack(nums, sum - nums[i], i + 1, cur, res);
                cur.remove(cur.size() - 1);
            }
        }
    }
}

131 Palindrome Partitioning

切割字符串,使得所有切分都是回文,求所有可能

https://leetcode.com/problems/palindrome-partitioning/

/*
算法复杂度,这个问题本质上是子集数的问题,所以总共有O(2^N)个节点,每个节点需要O(N)
来判断是否是回文,所以时间复杂度为O(N 2^N)
空间复杂度为递归深度,O(N)
Runtime: 7 ms, faster than 85.32% of Java online submissions for Palindrome Partitioning.
        Memory Usage: 52.9 MB, less than 53.17% of Java online submissions for Palindrome Partitioning.
*/

public class SolutionV2 {

    public List<List<String>> partition(String s) {
        List<List<String>> res = new ArrayList<>();
        backtrack(0, s, new ArrayList<String>(), res);
        return res;
    }

    private void backtrack(int k, String s, ArrayList<String> cur, List<List<String>> res) {
        if (k == s.length()) res.add(new ArrayList<>(cur));
        else {
            for (int i = k; i < s.length(); i++) {
                if (!isPalindrome(s, k, i)) continue;
                cur.add(s.substring(k, i+1));
                backtrack(i + 1, s, cur, res);
                cur.remove(cur.size() - 1);
            }
        }
    }

    private boolean isPalindrome(String sub, int low, int high) {
        for (; low < high; low++, high--)
            if (sub.charAt(low) != sub.charAt(high)) return false;
        return true;
    }
}

Ref

  • 全部灵感来自 https://leetcode.com/problems/combination-sum/discuss/16502/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partitioning)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值