LeetCode刷题之回溯

1,120 阅读22分钟

回溯

DFS用来解决可达性问题,而回溯主要用来解决排列组合问题,例如一个字符串“abc”能划分为几种不同的子序列,回溯在执行到某个特定位置会返回,所以需要在进行新的递归时将当前元素标记为“已访问”,但是在递归返回时需要将当前元素标为“未访问”。


17.电话号码的字母组合(Medium)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

解法一:这题是典型的排列组合问题,可以使用回溯的方法求解

class Solution {
    //建立数字与字母的映射
    private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public List<String> letterCombinations(String digits) {
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0){
            return res;
        }
        StringBuilder sb = new StringBuilder();
        doCombination(sb, digits, res);
        return res;
    }

    //递归函数
    private void doCombination(StringBuilder prefix, String digits, List<String> res) {
        //拼接长度等于字符长度
        if (prefix.length() == digits.length()){
            //添加进结果集
            res.add(prefix.toString());
            return;
        }
        //当前要添加的数字
        int curDigits = digits.charAt(prefix.length()) - '0';
        String letters = KEYS[curDigits];
        for (char c : letters.toCharArray()) {
            prefix.append(c); //添加
            //递归
            doCombination(prefix, digits, res);
            prefix.deleteCharAt(prefix.length() - 1); //删除
        }
    }
}

解法二:这道题还可以使用多层遍历来做。


93. 复原IP地址(Medium)

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

示例:

输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]

题解:ip地址一共有四段,每段从0~225,长度为1~3位,当一位时,可以为0~9,但是为两位或者三位时,不能以0开头,那么就需要一个函数来判断分的字符串是否合法,求所有可能的组成情况,可以使用递归。

class Solution {
    public List<String> restoreIpAddresses(String s) {
        List<String> res = new ArrayList<>();
        StringBuilder tempAddress = new StringBuilder();
        doRestore(0, s, res, tempAddress);
        return res;
    }

    private void doRestore(int k, String s, List<String> res, StringBuilder tempAddress) {
        if (k == 4 || s.length() == 0){
            //分了四段的时候就将结果添加进res
            if (k == 4 && s.length() == 0){
                res.add(tempAddress.toString());
            }
            //s为空且k!=4,直接返回,说明长度不够
            return;
        }

        //分别以长度1,2,3切分
        for (int i = 0; i < s.length() && i <= 2; i++) {
            //当为0x或者0xx的形式的时候,无效
            if (i != 0 && s.charAt(0) == '0'){
                break;
            }
            //切分
            String part = s.substring(0, i + 1);
            if (Integer.valueOf(part) <= 255){
                if (tempAddress.length() != 0) {
                    part = "." + part;
                }
                tempAddress.append(part);
                doRestore(k + 1, s.substring(i + 1), res, tempAddress);
                tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length());
            }
        }
    }
}

79. 单词搜索(Medium)

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
 ['A','B','C','E'],
 ['S','F','C','S'],
 ['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

题解:这道题是一道典型的DFS问题,单元格的字母不允许重复使用,所以需要准备一个跟数组同大小的布尔类型的数组visited,用来记录哪些元素已经访问过。遍历数组board,当遇到跟单词的第一个字母相等的就对四个方向进行DFS遍历,如果找到就返回true,没有就返回false。这个字母开始的遍历在返回的时候需要标记为“未访问过”

class Solution {
    //定义四个方向
    private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    private int m;
    private int n;

    public boolean exist(char[][] board, String word) {
        if (word == null || word.length() == 0){
            return true;
        }
        if (board == null || board.length == 0 || board[0].length == 0) {
            return false;
        }

        m = board.length;
        n = board[0].length;
        //准备一个数组来记录当前元素是否访问过
        boolean[][] vistied = new boolean[m][n];

        //遍历数组,进行dfs
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                //如果找到,就返回true
                if (backTracking(board, word, vistied, 0, i, j)){
                    return true;
                }
            }
        }

        return false;
    }

    private boolean backTracking(char[][] board, String word, boolean[][] vistied, int curWordLen, int i, int j) {
        //如果单词长度等于要找的长度,返回true
        if (curWordLen == word.length()){
            return true;
        }

        //如果超出边界或者当前字母不等于单词字母,返回false
        if (i < 0 || i >= m || j < 0 || j >= n || vistied[i][j] == true || board[i][j] != word.charAt(curWordLen)){
            return false;
        }

        //标记为true
        vistied[i][j] = true;

        //往四个方向dfs
        for (int[] d : direction) {
            if (backTracking(board, word, vistied, curWordLen + 1, i + d[0], j + d[1])){
                return true;
            }
        }

        vistied[i][j] = false;

        return false;
    }
}

257.二叉树的所有路径(Easy)

给定一个二叉树,返回所有从根节点到叶子节点的路径。

说明: 叶子节点是指没有子节点的节点。

示例:

输入:
 1
/   \
2   3
 \
  5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3

题解:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> paths = new ArrayList<>();
        if (root == null){
            return paths;
        }

        List<Integer> values = new ArrayList<>();//存储当前路径
        backTracking(root, paths, values);
        return paths;
    }

    //递归
    private void backTracking(TreeNode node, List<String> paths, List<Integer> values) {
        //空节点,直接返回
        if (node == null){
            return;
        }
        values.add(node.val);
        //子节点,将当前路径添加进paths
        if (isLeaf(node)){
            paths.add(buildPath(values));
        }else {
            //不是子节点,就往左右子树遍历
            backTracking(node.left, paths, values);
            backTracking(node.right, paths, values);
        }
        
        //删除当前节点
        values.remove(values.size() - 1);
    }

    private boolean isLeaf(TreeNode node) {
        return node.left == null && node.right == null;
    }

    private String buildPath(List<Integer> values) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < values.size(); i++) {
            sb.append(values.get(i));
            if (i != values.size() - 1){
                sb.append("->");
            }
        }
        return sb.toString();
    }
}

46. 全排列(Medium)

给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
 [1,2,3],
 [1,3,2],
 [2,1,3],
 [2,3,1],
 [3,1,2],
 [3,2,1]
]

题解一:这道题是个典型的全排列问题,可以用递归dfs解决,准备一个数组来标记当前元素是否被访问过,每个位置上所有数都有出现,遇到出现过的直接跳过,寻找下一个。

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return res;
        }
        List<Integer> temp = new ArrayList<>(); //记录当前排列
        boolean[] visited = new boolean[nums.length]; //访问标记
        backTracking(nums, res, temp, visited);
        return res;
    }

    private void backTracking(int[] nums, List<List<Integer>> res, List<Integer> temp, boolean[] visited) {
        if (nums.length == temp.size()){
            res.add(new ArrayList<>(temp));
            return;
        }
        //当前空格可以放nums中所有元素
        for (int i = 0; i < visited.length; i++) {
            //使用过的元素跳过
            if (visited[i]){
                continue;
            }
            visited[i] = true; //标记为已访问
            temp.add(nums[i]);
            backTracking(nums, res, temp, visited);
            temp.remove(temp.size() - 1); //回溯的时候要删除
            visited[i] = false; //标记为未访问过
        }
    }
}

题解二:数组上的第一个元素可以和其他任意元素交换位置,可以使用递归交换来完成全排列

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return res;
        }
        exchange(nums, 0, res);
        return res;
    }

    //递归交换
    private void exchange(int[] nums, int start, List<List<Integer>> res) {
        if (start >= nums.length){
            List<Integer> list = toList(nums);
            res.add(new ArrayList<>(list));
            return;
        }
        //当前元素与其他元素交换
        for (int i = start; i < nums.length; i++) {
            swap(nums, start, i);
            exchange(nums, start + 1, res);
            swap(nums, start, i);
        }
    }

    private void swap(int[] nums, int start, int i) {
        int temp = nums[start];
        nums[start] = nums[i];
        nums[i] = temp;
    }
    
    //将数组转换成list
    private List<Integer> toList(int[] nums) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0 i < nums.length; i++) {
            list.add(nums[i]);
        }
        return list;
    }
}

47. 全排列 II(Medium)

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例:

输入: [1,1,2]
输出:
[
 [1,1,2],
 [1,2,1],
 [2,1,1]
]

题解:这道题是上道题的变形,不同之处是这道题有重复元素,如果结果是数字的话可以用HashSet来去重,但是list不能使用,先对数组进行排序,便于发现重复的部分,所以用剪枝+递归的办法解决这道题。具体的分析可以参考LeetCode上一个大佬的分析

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return res;
        }
        Arrays.sort(nums); //先排序
        //准备一个临时list来存储结果
        List<Integer> tempList = new ArrayList<>();
        //准备一个数组来标记数组是否访问过
        boolean[] visited = new boolean[nums.length];
        backTracking(nums, visited, res, tempList);
        return res;
    }

    private void backTracking(int[] nums, boolean[] visited, List<List<Integer>> res, List<Integer> tempList) {
        //递归结束条件:
        if (tempList.size() == nums.length){
            res.add(new ArrayList<>(tempList));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            //剪枝1
            if (visited[i]){
                continue;
            }
            //剪枝2:如果不在第一层且当前分支等于上一分支数字,且上个分支没访问过('!'加不加结果相同),直接跳过
            if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]){
                continue;
            }
            visited[i] = true;
            tempList.add(nums[i]);
            backTracking(nums, visited, res, tempList);
            //回溯
            tempList.remove(tempList.size() - 1);
            visited[i] = false;
        }
    }
}

77. 组合(Medium)

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2 输出:
[
 [2,4],
 [3,4],
 [2,3],
 [1,2],
 [1,3],
 [1,4],
]

题解一:这道题是一道典型的回溯题,因为让我们求数的组合,所以可以用dfs+递归的方法求解。

  • 准备一个大列表res来存储结果,一个小列表tempList存储目前的结果
  • 当tempList == k时,添加到res
class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        if (k <= 0 || n <= 0 || k > n){
            return res;
        }
        List<Integer> tempList = new ArrayList<>();
        backTracking(res, tempList, n, k, 1);
        return res;
    }

    private void backTracking(List<List<Integer>> res, List<Integer> tempList, int n, int k, int start) {
        //tempList存够了k个,添加到res
        if (tempList.size() == k){
            res.add(new ArrayList<>(tempList));
            return;
        }
        //从start到n
        for (int i = start; i <= n; i++) {
            tempList.add(i);
            backTracking(res, tempList, n, k, i + 1);
            //回溯
            tempList.remove(tempList.size() - 1);
        }
    }
}

题解二:优化后的题解一,核心思想一样,但是进行了剪枝,在进行for循环的时候,不用遍历到n,只需要遍历到需要的长度就够了,比如n=5,k=3,在start=1时,遍历到3就够了,因为从4开始只有4和5两个数,不满足要求。处理后的时间会快很多,从击败50%左右变成99.97%。因为我们最后取到的是5,所以n - (k - tempList.size()) + 1,要有个+1

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        if (k <= 0 || n <= 0 || k > n){
            return res;
        }
        List<Integer> tempList = new ArrayList<>();
        backTracking(res, tempList, n, k, 1);
        return res;
    }

    private void backTracking(List<List<Integer>> res, List<Integer> tempList, int n, int k, int start) {
        //tempList存够了k个,添加到res
        if (tempList.size() == k){
            res.add(new ArrayList<>(tempList));
            return;
        }
        //从start到n
        for (int i = start; i <= n - (k - tempList.size()) + 1; i++) {
            tempList.add(i);
            backTracking(res, tempList, n, k, i + 1);
            //回溯
            tempList.remove(tempList.size() - 1);
        }
    }
}

题解三:先找出k=1的所有可能,然后再逐步+1

class solution{
    public List<List<Integer>> combine(int n, int k) {
        if (n == 0 || k == 0 || k > n) return Collections.emptyList();
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        //个数为 1 的所有可能
        for (int i = 1; i <= n + 1 - k; i++) res.add(Arrays.asList(i));
        //第一层循环,从 2 到 k
        for (int i = 2; i <= k; i++) {
            List<List<Integer>> tmp = new ArrayList<List<Integer>>();
            //第二层循环,遍历之前所有的结果
            for (List<Integer> list : res) {
                //第三次循环,对每个结果进行扩展
                //从最后一个元素加 1 开始,然后不是到 n ,而是和解法一的优化一样
                //(k - (i - 1) 代表当前已经有的个数,最后再加 1 是因为取了 n
                for (int m = list.get(list.size() - 1) + 1; m <= n - (k - (i - 1)) + 1; m++) {
                    List<Integer> newList = new ArrayList<Integer>(list);
                    newList.add(m);
                    tmp.add(newList);
                }
            }
            res = tmp;
        }
        return res;
    }
}

39. 组合总和(Medium)

给定一个无重复元素的数组 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]
]

题解:这道题跟前面三道题解法基本一致,同样是求“全解”,一般这种题目使用dfs+递归解决

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        if (candidates == null || candidates.length == 0 || target <= 0){
            return res;
        }
        List<Integer> tempList = new ArrayList<>();
        backTracking(candidates, target, res, tempList, 0);
        return res;
    }

    private void backTracking(int[] candidates, int target, List<List<Integer>> res, List<Integer> tempList, int start) {
        //减太多,直接返回
        if (target < 0){
            return;
        }
        //刚好为0,添加
        if (0 == target){
            res.add(new ArrayList<>(tempList));
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            tempList.add(candidates[i]);
            //递归
            backTracking(candidates, target - candidates[i], res, tempList, i);
            //回溯
            tempList.remove(tempList.size() - 1);
        }
    }
}

40. 组合总和 II(Medium)

给定一个数组 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]
]

题解:这道题和上道题区别不大,这道题要求一个数组中的元素不能使用多次,先将数组排序,这步很关键,用来保证不会重复使用同一个元素,然后在for循环的时候进行剪枝(这道题的解答参考了LeetCode上liweiwei1419的解答)

class Solution {
    
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        // 先将数组排序,这一步很关键
        Arrays.sort(candidates);
        findCombinationSum2(candidates, 0, len, target, new Stack<>(), res);
        return res;
    }

    // residue 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
    // residue 在递归遍历中,只会越来越小
    private void findCombinationSum2(int[] candidates, int begin, int len, int residue, Stack<Integer> stack, List<List<Integer>> res) {
        if (residue == 0) {
            res.add(new ArrayList<>(stack));
            return;
        }
        for (int i = begin; i < len && residue - candidates[i] >= 0; i++) {
            // 这一步之所以能够生效,其前提是数组一定是排好序的,这样才能保证:
            // 在递归调用的统一深度(层)中,一个元素只使用一次。
            // 这一步剪枝操作基于 candidates 数组是排序数组的前提下
            if (i > begin && candidates[i] == candidates[i - 1]) {
                continue;
            }
            stack.add(candidates[i]);
            // 【关键】因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
            findCombinationSum2(candidates, i + 1, len, residue - candidates[i], stack, res);
            stack.pop();
        }
    }
}

216. 组合总和 III(Medium)

找出所有相加之和为 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]]

题解:这道题和前面两道题相比变化较大,跟77题解法较为相似

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> res = new ArrayList<>();
        if (n <= 0 || k == 0){
            return res;
        }
        List<Integer> tempList = new ArrayList<>();
        backTracking(k, n, 1, res, tempList);
        return res;
    }

    private void backTracking(int k, int n, int level, List<List<Integer>> res, List<Integer> tempList) {
        if (k == 0 && n == 0){
            res.add(new ArrayList<>(tempList));
            return;
        }
        if (k == 0 || n == 0){
            return;
        }
        for (int i = level; i <= 9; i++) {
            tempList.add(i);
            backTracking(k - 1, n - i, i + 1, res, tempList);
            tempList.remove(tempList.size() - 1);
        }
    }
}

78.子集(Medium)

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

解法一:参考一个大佬的做法,可以一步一步叠加,先是一个[],然后添加1就得到了[1],再增加2,得到[2],[1,2],这样一直添加,直到最后一个数。

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            res.add(new ArrayList<>());
            return res;
        }
        //初始化空数组
        res.add(new ArrayList<>());
        for (int i = 0; i < nums.length; i++) {
            //准备一个临时结果集
            List<List<Integer>> tmpRes = new ArrayList<>();
            //遍历之前的所有结果
            for (List<Integer> list : res) {
                List<Integer> tmp = new ArrayList<>(list);
                tmp.add(nums[i]); //往之前结果添加
                tmpRes.add(tmp);
            }
            res.addAll(tmpRes);
        }
        return res;
    }
}

解法二:使用递归+dfs来解决,整个数组组成子集的时候,都可以选择添加当前元素或者不添加当前元素。经典的回溯法

class Solution{
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            res.add(new ArrayList<>());
            return res;
        }
        List<Integer> tempList = new ArrayList<>();
        backTracking(0, nums, res, tempList);
        return res;
    }

    private void backTracking(int start, int[] nums, List<List<Integer>> res, List<Integer> tempList) {
        //添加进结果
        res.add(new ArrayList<>(tempList));
        for (int i = start; i < nums.length; i++) {
            //选当前元素
            tempList.add(nums[i]);
            backTracking(i + 1, nums, res, tempList);
            //不选
            tempList.remove(tempList.size() - 1);
        }
    }
}

解法三:看评论区的时候发现有位运算这种操作,不仅感叹真是人才众多,脑洞大开,以{1,2,3}为例,[0,0,0]表示空集,[1,0,0]表示{1},以此类推

public class Solution {
    public List<List<Integer>> subsets(int[] nums) {
    
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            res.add(new ArrayList<>());
            return res;
        }

        int bit_nums = nums.length;
        int res_nums = 1 << bit_nums;//2的n次方

        for (int i = 0; i < res_nums; i++) {
            List<Integer> tmp = new ArrayList<>();
            //nums中哪位上是1,就添加进去
            for (int j = 0; j < nums.length; j++) {
                if ((i & (1 << j)) != 0){
                    tmp.add(nums[j]);
                }
            }
            res.add(tmp);
        }
        return res;
    }
}

90. 子集 II(Medium)

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

题解:这道题是上一道题的延伸,所以有些解法可以复用,只是需要稍加改动,子集长度可以为0~nums.length;所以可以求每种长度的可能的子集,然后添加到结果集,循环过程中还需要进行剪枝

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            res.add(new ArrayList<>());
            return res;
        }
        boolean[] visited = new boolean[nums.length];
        List<Integer> tempList = new ArrayList<>();
        Arrays.sort(nums); //排序
        //获取各种长度的子集
        for (int size = 0; size <= nums.length; size++) {
            backTracking(0, nums, res, tempList, visited, size);
        }
        return res;
    }


    private void backTracking(int start, int[] nums, List<List<Integer>> res, List<Integer> tempList, boolean[] visited, int size) {
        //添加进结果
        if (tempList.size() == size) {
            res.add(new ArrayList<>(tempList));
            return;
        }
        for (int i = start; i < nums.length; i++) {
            //剪枝
            if (i != 0 && nums[i] == nums[i - 1] && visited[i - 1]){
                continue;
            }
            //选当前元素
            tempList.add(nums[i]);
            visited[i] = true;
            backTracking(i + 1, nums, res, tempList, visited, size);
            //不选
            visited[i] = false;
            tempList.remove(tempList.size() - 1);
        }
    }
}

解法二:利用上题解法一的思路,稍作改变,本题是有重复元素的,所以添加的时候要注意不能重复添加的问题,以[1, 2, 2]为例,[]-->[] [1]-->[] [1] [2] [1,2]-->?这时候继续添加2的时候,要注意只能从“[2] [1,2]”后面添加,前面两个不能添加,所以需要准备一个变量last来记录当前元素是否跟前面元素相等,不等,就从上个集合的0开始,相等,新子集个数减去之前循环时子集的个数当做起点来循环。

class Solution {
    public static List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0){
            res.add(new ArrayList<>());
            return res;
        }
        Arrays.sort(nums);
        int size = 1; //记录前一个集合的长度
        int last = nums[0];

        //初始化空数组
        res.add(new ArrayList<>());
        for (int i = 0; i < nums.length; i++) {
            //如果不等
            if (last != nums[i]) {
                last = nums[i];
                size = res.size();
            }
            //准备一个临时结果集
            List<List<Integer>> tmpRes = new ArrayList<>();
            int newSize = res.size();
            //遍历之前的所有结果
            for (int j = newSize - size; j < newSize; ++j) {
                List<Integer> tmp = new ArrayList<>(res.get(j));
                tmp.add(nums[i]); //往之前结果添加
                tmpRes.add(tmp);
            }
            //更新上个list长度
            res.addAll(tmpRes);
            size = res.size() - tmpRes.size();
        }
        return res;
    }
}

131. 分割回文串(Medium)

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[
  ["aa","b"],
  ["a","a","b"]
]

题解:题目让我们找到一个字符串所有可能的回文子串,那么需要先做的是将字符串的所有子串找到,然后再逐个判断是否是回文串(用一个函数),优化一下,可以在找子串的过程中判断,如果是回文串的就添加进去,否则跳过。(分治解法)

class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> res = new ArrayList<>();
        if (s == null){
            res.add(new ArrayList<>());
            return res;
        }
        List<String> tempList = new ArrayList<>();
        backTracking(s, res, tempList);
        return res;
    }

    //回溯
    private void backTracking(String s, List<List<String>> res, List<String> tempList) {
        //当s被分完之后,将结果添加到结果集
        if (s.length() == 0){
            res.add(new ArrayList<>(tempList));
            return;
        }
        
        //从每个字符开始,遍历字符串s,找到所有的子串
        for (int i = 0; i < s.length(); i++) {
            //如果为回文结构
            if (isPalindrome(s, 0, i)){
                tempList.add(s.substring(0, i + 1)); //添加进临时list
                backTracking(s.substring(i + 1), res, tempList);
                tempList.remove(tempList.size() - 1);
            }
        }
    }

    //判断一个字符串是否为回文结构
    private boolean isPalindrome(String s, int begin, int end){
        while (begin < end){
            if (s.charAt(begin++) != s.charAt(end--)){
                return false;
            }
        }
        return true;
    }
}

37. 解数独(Hard)

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 '.' 表示。

答案被标成红色。

Note:

给定的数独序列只包含数字 1-9 和字符 '.' 。
你可以假设给定的数独只有唯一解。
给定数独永远是 9x9 形式的。

class Solution {
    public void solveSudoku(char[][] board) {
        backTracking(board, 0, 0);
    }

    //递归函数
    private boolean backTracking(char[][] board, int i, int j) {
        //如果遍历完了最后一行,返回true
        if (i == 9){
            return true;
        }
        //若遍历完了当前行,跳到下一行
        if (j >= 9){
            return backTracking(board, i + 1, 0);
        }
        //如果当前行不为',',直接跳过,到下一个数
        if (board[i][j] != '.'){
            return backTracking(board, i, j + 1);
        }
        
        //对当前格子进行dfs
        for (char k = '1'; k <= '9'; k++) {
            //如果当前数字不能填充,跳过,换下一个
            if (!isValid(board, i, j, k)){
                continue;
            }
            board[i][j] = k;
            if (backTracking(board, i, j + 1)){
                return true;
            }
            //重置
            board[i][j] = '.';
        }
        return false;
    }

    private boolean isValid(char[][] board, int i, int j, char val) {
        //同一列唯一
        for (int x = 0; x < 9; ++x) {
            if (board[x][j] == val) {
                return false;
            }
        }
        //同一行唯一
        for (int y = 0; y < 9; ++y) {
            if (board[i][y] == val) {
                return false;
            }
        }
        //实线包围的九宫格唯一
        int row = i - i % 3, col = j - j % 3;
        for (int x = 0; x < 3; ++x) {
            for (int y = 0; y < 3; ++y) {
                if (board[x + row][y + col] == val) {
                    return false;
                }
            }
        }
        return true;
    }
}

51. N皇后(Hard)

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例:

输入: 4
输出: [
[".Q..", // 解法 1
 "...Q",
 "Q...",
 "..Q."],

["..Q.", // 解法 2
 "Q...",
 "...Q",
 ".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

题解:这道题的解法跟数独类似,都是遍历数组,进行dfs,然后判断皇后是否能放在当前格子,可以的话进行dfs,不行的话就跳到下一个格子。判断是否有效的时候只需判断同一列,两个对角线即可,因为同一行是遍历的,不用判断,每次都只取其中一个。

class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> res = new ArrayList<>();
        if (n == 0){
            return res;
        }

        //初始化queens
        char[][] queens = new char[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(queens[i], '.');
        }

        backTracking(0, res, queens, n);
        return res;
    }

    //回溯
    private void backTracking(int curRow, List<List<String>> res,
                              char[][] queens, int n) {
        //如果遍历完了n行,说明找到一种解
        if (curRow == n){
            List<String> list = new ArrayList<>();
            for (char[] queen : queens) {
                list.add(new String(queen));
            }
            res.add(list);
            return;
        }

        //遍历每一行
        for (int i = 0; i < n; i++) {
            //当前格子能够放置皇后
            if (isValid(queens, curRow, i)){
                queens[curRow][i] = 'Q';
                backTracking(curRow + 1, res, queens, n);
                queens[curRow][i] = '.';
            }
        }
    }

    boolean isValid(char[][]queens, int row, int col) {
        //判断同一列
        for (int i = 0; i < row; ++i) {
            if (queens[i][col] == 'Q') {
                return false;
            }
        }
        //判断45°
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
            if (queens[i][j] == 'Q') {
                return false;
            }
        }
        //判断135°
        for (int i = row - 1, j = col + 1; i >= 0 && j < queens.length; --i, ++j) {
            if (queens[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
}