回溯算法总结——团灭组合、排列、切割、N皇后等问题

回溯算法总结

  • 1. 概述
  • 2. 组合问题
    • 2.1 组合
    • 2.2 组合总和
    • 2.3 组合总和 II
    • 2.4 组合总和 III
    • 2.5 组合问题小结
  • 3. 排列问题
    • 3.1 全排列
    • 3.2 全排列 II
  • 4. 子集问题
    • 4.1 子集
    • 4.2 子集 II
    • 4.3 递增子序列
    • 4.4 划分为k个相等的子集
  • 5 棋盘问题
    • 5.1 N皇后
    • 5.2 解数独
  • 思考
  • 参考

1. 概述

回溯大体分为:组合、排列、子集、切割、搜索几种类型

类型 题目链接 思路
组合问题 77.组合
39.组合总和
40. 组合总和 II
216. 组合总和 III
排列问题 46. 全排列
47. 全排列 II
子集问题 78. 子集
90. 子集 II
491. 递增子序列
切割问题 131. 分割回文串
93. 复原 IP 地址
搜索问题 51. N 皇后
37. 解数独

2. 组合问题

2.1 组合

LC链接:77.组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4],]

  • 思路:
  1. 组合问题,使用 start (indexStart) 来逐层缩小搜索范围。比如这层选择了 i ,那么递归到下层时,应该从 i + 1 开始进行迭代。
  2. 递归何时结束,当 path 中的元素数量等于 k 时,即可结束。
    回溯算法总结——团灭组合、排列、切割、N皇后等问题_第1张图片
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;

class Solution {
    
    List<List<Integer>> res;
    
    Deque<Integer> path;
    
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList();
        // 固定path的大小,防止运行中反复扩容带来的性能损失
        path = new ArrayDeque(k);
        backtracking(n, k, 1);
        return res;
    }
    
    private void backtracking(int n, int k, int start) {
        // 当 path 中的元素数量等于k时,返回
        if (path.size() == k) {
            res.add(new ArrayList(path));
            return;
        }
        // 下层遍历从 i+1 开始
        for (int number = start; number <= n; number++) {
            path.addLast(number);
            backtracking(n, k, number + 1);
            path.removeLast();
        }
    }
}
  • 剪枝优化:当剩余的数全部添加到 path 中,也不足以使得 path.size() == k 时,即可结束搜索(当前层及后续层级)。比如n = 5, k = 4,表示需要从 [1, 2, 3, 4, 5] 中选取4个元素。当程序运行到某个时刻,假设此时path.size()为1,那么还需要3个元素添加到 path 中才能满足 k = 4 这个条件,因此就不能从 4 及 4 以后开始进行遍历,因为即使把所有这些元素加入到path中,也无法满足 k = 4 这个条件。
  1. path.size() 表示当前路径元素的个数
  2. k - path.size() 表示还需要的元素个数
  3. 所以上述代码中 number 最大只能到 n - (k - path.size()) + 1
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;

class Solution {
    
    List<List<Integer>> res;
    
    Deque<Integer> path;
    
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList();
        // 固定path的大小,防止运行中反复扩容带来的性能损失
        path = new ArrayDeque(k);
        backtracking(n, k, 1);
        return res;
    }
    
    private void backtracking(int n, int k, int start) {
        // 当 path 中的元素数量等于k时,返回
        if (path.size() == k) {
            res.add(new ArrayList(path));
            return;
        }
        // 下层遍历从 i+1 开始
        // 剪枝优化
        for (int number = start; number <= n - (k - path.size()) + 1; number++) {
            path.addLast(number);
            backtracking(n, k, number + 1);
            path.removeLast();
        }
    }
}

优化前后运行时间对比,能看到剪枝后速度提升了19倍!
回溯算法总结——团灭组合、排列、切割、N皇后等问题_第2张图片


2.2 组合总和

LC链接:39.组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1: 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
示例 2: 输入:candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

  • 思路:
  1. 求组合需要利用 indexStart 缩小搜索范围,但是本题中每个元素可以使用次数的不限,所以下一次遍历的indexStart等于本次遍历的indexStart,而不需要加1
  2. 递归何时结束,由于目前还需要的 target 是由原 target 一路减去 path 中的数字而传递下来的,所以当 target 小于等于 0 的时候,退出。特别地,当 target 等于0 的时候说明找到了和为原 target 的路径,需要将路径添加到最终的 res 中。

回溯算法总结——团灭组合、排列、切割、N皇后等问题_第3张图片

import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;

class Solution {
    
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtracking(candidates, target, 0);
        return res;
    }
    
    private void backtracking(int[] candidates, int remain, int indexStart) {
        // target小于0 退出
        if (remain < 0) return;
        
        // target等于于0,添加路径、退出
        if (remain == 0) {
            res.add(new ArrayList(path));
            return;
        }
        
        for (int i = indexStart; i < candidates.length; i++) {
            path.addLast(candidates[i]);
            // 每个数字可以使用无限次,所以下次还从i开始遍历即可
            backtracking(candidates, remain - candidates[i], i);
            path.removeLast();
        }
         
    }
}
  • 剪枝:先对原数组进行排序,然后在遍历过程中如果发现如果 remain < candidates[i],则当candidates[i]或者 candidates[i] 之后的数字再加入 path 中,会导致 path 中的数字和超过target ,所以一旦出现 remain < candidates[i],即可终止。代码如下:
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;

class Solution {
    
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target <= 0) return res;
        Arrays.sort(candidates);
        backtracking(candidates, target, 0);
        return res;
    }
    
    private void backtracking(int[] candidates, int remain, int indexStart) {
        // target小于0 退出
        if (remain < 0) return;
        
        // target等于于0,添加路径、退出
        if (remain == 0) {
            res.add(new ArrayList(path));
            return;
        }
        // 剪枝 remain >= candidates[i],如果 remain < candidates[i] 说明后续的数再加入path会导致path数字和超过原target
        for (int i = indexStart; i < candidates.length && remain >= candidates[i]; i++) {
            path.addLast(candidates[i]);
            // 每个数字可以使用无限次,所以下次还从i开始遍历即可
            backtracking(candidates, remain - candidates[i], i);
            path.removeLast();
        }
         
    }
}

剪枝后,LC上运行时间从14ms降到3ms


2.3 组合总和 II

LC链接:40. 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。
示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]

  • 思路:
  1. 使用 indexStart 来逐层缩小搜索范围:该题是求组合,每个索引对应的元素只能使用一次,虽然数组中的元素可能重复(如例子 [10,1,2,7,6,1,5] 中,索引1和索引5对应的元素都是1),但是最终结果集中的每个组合都是对应索引只使用了一次,比如[1, 1, 6] 里的两个1分别对应[10,1,2,7,6,1,5] 数组索引1和索引5的位置。
  2. 树层去重:需要对原数组先排序!如果不排序的话,示例2中会出现 [2, 2, 1][2, 1, 2] 的组合,但其实这两个算作一个组合。当然,也可以当把这两个组合都求出来后,在最后添加进结果集的时候再去重,但是这样做了很多无用功,势必会超时。而排序后,上述两个组合都变成 [1, 2, 2],这也能够在搜索过程中进行去重。

回溯算法总结——团灭组合、排列、切割、N皇后等问题_第4张图片

树层去重1:使用used 数组:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过
class Solution {
    
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] used = new boolean[candidates.length];
        backtracking(candidates, target, 0, used);
        return res;
    }
    
    private void backtracking(int[] candidates, int target, int indexStart, boolean[] used) {
        if (target == 0) {
            res.add(new ArrayList(path));
            return;
        }
        
        for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
            used[i] = true;
            path.addLast(candidates[i]);
            backtracking(candidates, target - candidates[i], i + 1, used);
            path.removeLast();
            used[i] = false;
            
        }
    }
}

树层去重2: 三数之和的思想,其实这种思想有类似点 15. 三数之和 ,不管是求和的目的,还是去重的逻辑。

  1. 先排序
  2. if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;

class Solution {
    List<List<Integer>> res = new ArrayList();

    Deque<Integer> path = new LinkedList();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracking(candidates, target, 0);
        return res;
    }

    private void backtracking(int[] candidates, int target, int indexStart) {
        // target 每添加一个元素到 path,target 都会减去悉新加入元素的值,当 target 为0的时候,说明 path 里元素和等于 target
        if (target == 0) {
            res.add(new ArrayList(path));
            return;
        }

        // 每次从 indexStart 开始遍历
        // 且由于数组排序过,当剩余的 target < candidates[i]时,说明再将 candidates[i] 加入path,path里的元素和将会大于 target,
        // 所以对于candidates[i] candidates[i+1] ... 不用再遍历
        for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
            // 树层去重,注意 i > indexStart
            if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
            path.addLast(candidates[i]);
            backtracking(candidates, target - candidates[i], i + 1);
            path.removeLast();
        }
    }
}

树层去重3:桶的思想,由于 1 <= candidates[i] <= 50 ,我们为每一层创建一个桶(数组),如果每一层上某个数字已经使用过,就跳过

class Solution {
    
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracking(candidates, target, 0);
        return res;
    }
    
    private void backtracking(int[] candidates, int target, int indexStart) {
        if (target == 0) {
            res.add(new ArrayList(path));
            return;
        }
        // 1 <= candidates[i] <= 50 记录每层数字是否使用过
        boolean[] used = new boolean[51];
        
        for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
            if (used[candidates[i]] == true) continue;
            used[candidates[i]] = true;
            path.addLast(candidates[i]);
            backtracking(candidates, target - candidates[i], i + 1);
            path.removeLast();            
        }
    }
}
  • 要点:
  1. 求组合需要利用 indexStart 缩小搜索范围,也是为了避免结果集中每个组合中的元素重复;
  2. 树层去重,有3种方法,推荐使用第一种,可以和树枝去重共用同一个数组。

2.4 组合总和 III

LC链接:216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

  • 思路:
  1. 组合问题考虑 start (indexStart)。由于题目中提到 “每种组合中不存在重复的数字”,所以需要利用 indexStart 来缩小搜索范围确保元素的之前使用过的元素不会再选取到。至于是使用 start 还是 indexStart 取决于我们在 for 循环中是使用索引进行遍历还是使用元素进行遍历。显然,本题直接使用元素进行遍历。
  2. 递归合适结束:当 path 中的元素个数等于k时,需要退出。并且当 remain 刚好为0时,说明 path 中所有数相加等于 n,所以此时需要将 path 添加到 res 中。
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;

class Solution {
    
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path;
    
    public List<List<Integer>> combinationSum3(int k, int n) {
        // 固定path的大小,防止运行中反复扩容带来的性能损失
        path = new ArrayDeque(k);
        backtrackin(k, n, 1);
        return res;
        
    }
    
    private void backtrackin(int k, int remain, int start) {   
        // 元素达到 k 个,需要返回。特别地,如果此时 remain 刚好为0,需要将路径 path 添加到 res 中
        if (path.size() == k) {
            if (remain == 0) {
                res.add(new ArrayList(path));
            }
            return;
        }
        
        // remain >= num 进行剪枝
        for (int num = start; num <= 9 && remain >= num; num++) {
            path.addLast(num);
            backtrackin(k, remain - num, num + 1);
            path.removeLast();
        }
    }
}

2.5 组合问题小结

  1. 组合问题一般都需要使用 indexStart (start) 来在递归中缩小搜索范围,避免组合中出现重复元素;
  2. 当写出基本版本的代码后,可以考虑是否能通过剪枝的方式提高效率;
  3. 树层去重的时候需要对原数组进行排序。

3. 排列问题

3.1 全排列

LC链接:46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

  • 思路:
  1. 每层从0开始便利,不用 indexStart :排列由于需要把数组中的数字(1、2、3)全部用到,所以不能像组合子集问题一样利用 indexStart 来逐步缩小问题范围,而是需要在每次递归时做完整的遍历(即从索引0开始遍历),把没有用到的元素添加到 path 中。
  2. 递归何时结束:当 path 中元素数量等于原数组中元素数量时,说明原数组元素远不用完,返回。
  3. 树枝去重:我们规定使用 usedPath 数组来记录一条路径(树枝)上对应索引元素的使用情况。
    回溯算法总结——团灭组合、排列、切割、N皇后等问题_第5张图片
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;

class Solution {
    List<List<Integer>> res = new ArrayList();

    Deque<Integer> path = new LinkedList();

    public List<List<Integer>> permute(int[] nums) {
        boolean[] usedPath = new boolean[nums.length];
        backtracking(nums, usedPath);
        return res;
    }

    // 排列,数组里的元素都要用到且只能用一次,不用像组合一样使用indexStart缩小取值范围
    // 而是使用 usedPath 数组来标记每一条路径(树枝)上元素的使用情况,如果已经在该路径上使用过就跳过
    private void backtracking(int[] nums, boolean[] usedPath) {
        // 退出条件:当路径元素跟数组元素相等时
        if (path.size() == nums.length) {
            res.add(new ArrayList(path));
            return;
        }

        // 排列每次都从0开始遍历,利用usedPath来确保每一条路径上元素的唯一性
        for (int i = 0; i < nums.length; i++) {
            if (usedPath[i] == true) continue;
            usedPath[i] = true;  // 记录该下标的元素在当前路径上已经使用过
            path.addLast(nums[i]);  // 将元素添加到当前路径上
            backtracking(nums, usedPath);  // 带着当前路径元素的使用记录,继续去(从下标0开始)遍历
            path.removeLast();  // 回溯路径
            usedPath[i] = false;  // 回溯路径元素使用记录
        }
    }
}
  • 要点:
  1. 树枝去重:排列问题需要每层都从0开始遍历不需要 indexStart,而是使用 usedPath 完成路径上的去重, usedPath 记录的是数组元素在每条路径上的使用情况——元素下标

3.2 全排列 II

LC链接:47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1: 输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2: 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10


  • 思路:利用 used 数组完成数层和树枝去重
class Solution {
    
    List<List<Integer>> res;
    Deque<Integer> path;
    public List<List<Integer>> permuteUnique(int[] nums) {
        res = new ArrayList();
        path = new ArrayDeque(nums.length);
        
        boolean[] used = new boolean[nums.length];
        Arrays.sort(nums);
        backtracking(nums, used);
        return res;
    }
    
    // 思路:在全排列的基础上增加树层去重
    private void backtracking(int[] nums, boolean[] used) {
        if (path.size() == nums.length) {
            res.add(new ArrayList(path));
            return;
        }
                
        for (int i = 0; i < nums.length; i++) {
            // 树层去重
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
            // 树枝去重
            if (used[i] == true) continue;

            used[i] = true;
            path.addLast(nums[i]);
            backtracking(nums, used);
            path.removeLast();
            used[i] = false;
            
        }
    }
}

  • 思路2:利用桶的思想完成数层去重,笔者目前还没有想通为什么这种方法不用对原数组排序???
class Solution {
    
    List<List<Integer>> res;
    Deque<Integer> path;
    public List<List<Integer>> permuteUnique(int[] nums) {
        res = new ArrayList();
        path = new ArrayDeque(nums.length);
        
        boolean[] usedPath = new boolean[nums.length];
        // Arrays.sort(nums);
        backtracking(nums, usedPath);
        return res;
    }
    
    // 思路:在全排列的基础上增加树层去重
    private void backtracking(int[] nums, boolean[] usedPath) {
        if (path.size() == nums.length) {
            res.add(new ArrayList(path));
            return;
        }
        
        boolean[] usedLayer = new boolean[21];
        
        for (int i = 0; i < nums.length; i++) {
            // 树层去重
            if (usedLayer[nums[i] + 10] == true) continue;
            // 树枝去重
            if (usedPath[i] == true) continue;

            usedLayer[nums[i] + 10] = true;
            usedPath[i] = true;
            path.addLast(nums[i]);
            backtracking(nums, usedPath);
            path.removeLast();
            usedPath[i] = false;
            
        }
    }
}

4. 子集问题

4.1 子集

LC链接:78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]

  • 思路:子集跟组合问题一样,也要求集合无序,所以需要 indexStart 来确保取过的元素不会重复添加。但是组合问题是到叶子结点才进行收集,而子集问题是收集所有节点。

回溯算法总结——团灭组合、排列、切割、N皇后等问题_第6张图片

class Solution {
    List<List<Integer>> res;
    
    Deque<Integer> path;
    
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList();
        path = new ArrayDeque(nums.length);
        backtracking(nums, 0);
        return res;
    }
    
    private void backtracking(int[] nums, int indexStart) {
        // 子集问题不需要主动退出,当遍历完数组自然结束即可
        
        res.add(new ArrayList(path));
                
        for (int i = indexStart; i < nums.length; i++) {
            path.addLast(nums[i]);
            backtracking(nums, i + 1);
            path.removeLast();
        }
    }
}

4.2 子集 II

90. 子集 II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

  • 思路(推荐):used 数组去重。原数组有重复,而要求的集合中不允许有重复,画树形图分析后可知需要树层去重。使用 used 数组结合 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue; 条件完成树层去重
class Solution {
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        backtracking(nums, 0, used);
        return res;
    }
    
    private void backtracking(int[] nums, int indexStart, boolean[] used) {
        res.add(new ArrayList(path));
        
        for (int i = indexStart; i < nums.length; i++) {
            // 树层去重
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
            used[i] = true;
            path.addLast(nums[i]);
            backtracking(nums, i + 1, used);
            path.removeLast();
            used[i] = false;
            
        }
    }
}

  • 思路(推荐):桶的思想去重。为每一层创建一个 usedLayer 数组用来在遍历某一层(for 循环)时,记录当前层的数字使用情况,由于题目限定 -10 <= nums[i] <= 10,所以usedLayer大小为 21,相当于把 [-10,10] 平移到 [0, 20],然后以下标来进行统计,下标 0 对应 nums 中的-10,下标 1 对应 nums 中的-9 … 下标 20 对应 nums 中的10。
class Solution {
    List<List<Integer>> res = new ArrayList();
    
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        backtracking(nums, 0);
        return res;
    }
    
    private void backtracking(int[] nums, int indexStart) {
        res.add(new ArrayList(path));
        
        boolean[] usedLayer = new boolean[21];
        
        for (int i = indexStart; i < nums.length; i++) {
            // 树层去重,桶的思想
            if (usedLayer[nums[i] + 10] == true) continue;
            usedLayer[nums[i] + 10] = true;
            path.addLast(nums[i]);
            backtracking(nums, i + 1);
            path.removeLast();
            
        }
    }
}

4.3 递增子序列

LC链接:491. 递增子序列

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

这道题坑的点还挺多的:
1、树层去重(不能排序)
2、大于才能添加到path中

class Solution {
    
    List<List<Integer>> res = new ArrayList();
    Deque<Integer> path = new LinkedList();
    
    public List<List<Integer>> findSubsequences(int[] nums) {
        // 不能排序,求递增子序列,如果排序就改变了数组原有的顺序
        backtracking(nums, 0);
        return res;
    }
    
    private void backtracking(int[] nums, int indexStrat) {
        if (path.size() >= 2) {
            res.add(new ArrayList(path));
        }
        
        boolean[] usedLayer = new boolean[201];
        
        for (int i = indexStrat; i < nums.length; i++) {
            // 树层去重
            if (usedLayer[nums[i] + 100] == true) continue;
            // 确保递增
            if (path.isEmpty() || nums[i] >= path.getLast()) {
                usedLayer[nums[i] + 100] = true;
                path.addLast(nums[i]);
                backtracking(nums, i + 1);
                path.removeLast();
            }
        }
    }
}

4.4 划分为k个相等的子集

LC链接:698. 划分为k个相等的子集

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000

5 棋盘问题

5.1 N皇后

5.2 解数独


思考

  • 三种树层去重的区别:
  1. used 数组既可以用来树层去重,也可以用来树枝去重,为什么 46. 全排列 树枝去重时使用 if (i > 0 && nums[i] == nums[i - 1] && usedPath[i - 1] == true) continue; 不对? 因为46题中的元素没有重复的?

  2. 说好的树层去重需要排序,但是为什么 47. 全排列 II 利用桶的思想去重时就不用排序?因为[2, 1, 2] 和 [2, 2, 1] 是不同的排列,但是属于同一个组合(子集)。

  3. 2.3、3.2、4.2三个题结合看,总结提炼树层去重的共性!

  4. 树层去重里的层指的是具有直接共同父节点的层,而不是层次遍历中指的一整层!

  • 凡是原数组有重复,或者原数组的数字可以重复使用的情况,都需要树层去重,且树层去重需要先对原数组排序

参考

  • 代码随想录

你可能感兴趣的:(Algorithm,算法,leetcode,递归法)