回溯算法技巧总结 permutation,subset,combinations

1. 回溯算法1 https://leetcode-cn.com/problems/permutations/

import java.util.ArrayList;
import java.util.List;

/**
 题目描述:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
 No77题目:返回n个数中k个数的组合   本题中每个排列都要用到所有的数字

 方法1:每一次选择两个数ij进行交换,for (int j = i; j <nums.length ; j++) 从i位置开始进行回溯
 方法2:采用一个visited[]数组标记已经访问过的数字,for(int i=0;i<n;i++)从0进行回溯
 */
public class No46_permute {
    public static void main(String[] args) {
        int[] arr={1,2,3};
        List<List<Integer>> lists=new ArrayList<>();
        lists=permute(arr);
        for (int i = 0; i <lists.size() ; i++) {
            System.out.println(lists.get(i));
        }
    }

    public static List<List<Integer>> permute(int[] nums) {
        if (nums==null||nums.length==0)
            return null;
        List<List<Integer>> lists=new ArrayList<>();
        List<Integer> list=new ArrayList<>();
        return dfs(nums,0,list,lists);
    }

    private static List<List<Integer>> dfs(int[] nums, int i, List<Integer> list, List<List<Integer>> lists) {
        if (i>nums.length)
            return lists;

        if (i==nums.length){
            lists.add(new ArrayList<>(list));
            return lists;}
        for (int j = i; j <nums.length ; j++) {
            swap(nums,i,j);//先交换再add。
            // j=i时,数组为【1,2,3】完成一次dfs,可以获得1,2,3和1,3,2;
            // 然后j=i+1,应该先交换i和j,得到数组【2,1,3】,再进行dfs,得到2,1,3和2,3,1;如果先add,再交换,则与之前的重复,交换失去了意义
            list.add(nums[i]);
            dfs(nums,i+1,list,lists);
            list.remove(list.size()-1);
            swap(nums,i,j);//每一次都应该换回交换前的状态,保证下次交换时,只与最初状态差一,否则将差太多
        }
        return lists;
    }

    private static void swap(int[] nums, int i, int j) {
        int temp=nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }


    /**
    ++++++++++++++++++++++++++++ 方法2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     */
    public List<List<Integer>> permute2(int[] nums) {
        // 首先是特判
        int len = nums.length;
        // 使用一个动态数组保存所有可能的全排列
        List<List<Integer>> res = new ArrayList<>();

        if (len == 0) {
            return res;
        }

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

        dfs(nums, len, 0, path, used, res);
        return res;
    }

    private void dfs(int[] nums, int len, int depth, List<Integer> path, boolean[] used, List<List<Integer>> res) {
        if (depth == len) {
            res.add(path);
            return;
        }

        for (int i = 0; i < len; i++) {
            if (!used[i]) {
                path.add(nums[i]);
                used[i] = true;
                dfs(nums, len, depth + 1, path, used, res);
                // 注意:这里是状态重置,是从深层结点回到浅层结点的过程,代码在形式上和递归之前是对称的
                used[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }

}

2.回溯算法2 https://leetcode-cn.com/problems/permutations-ii/

import java.util.*;

/**
 * 题目描述:给定一个可包含重复数字的序列,返回所有不重复的全排列。
 * dfs前,需要进行去重
 */
public class No47_permuteUnique {
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }

        // 排序(升序或者降序都可以),排序是剪枝的前提
        Arrays.sort(nums);

        boolean[] used = new boolean[len];
        // 使用 Deque 是 Java 官方 Stack 类的建议
        Deque<Integer> path = new ArrayDeque<>(len);
        dfs(nums, len, 0, used, path, res);
        return res;
    }

    private void dfs(int[] nums, int len, int depth, boolean[] used, Deque<Integer> path, List<List<Integer>> res) {
        if (depth == len) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < len; ++i) {
            if (used[i]) continue;

            // 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
            // 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }

            path.addLast(nums[i]);
            used[i] = true;

            dfs(nums, len, depth + 1, used, path, res);
            // 回溯部分的代码,和 dfs 之前的代码是对称的
            used[i] = false;
            path.removeLast();
        }
    }
}

3.组合 https://leetcode-cn.com/problems/combinations/

import java.util.ArrayList;
import java.util.List;
//题目描述:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
//思路:采用dfs 回溯

/**
 * 分析:
 * 1.返回结果:List<List<Integer>>
 * 2.dfs工具:第一步肯定要传入n,k,lists,另外还有一个index表示当前递归到哪一个数了
 * 2.1 递归终止条件:递归了k次
 * 2.2 从当前数字开始进行递归,每一次尝试添加数字,进行dfs
 * 2.3 不管成功与否都进行回溯
 */
public class No77_combinations {
    public static void main(String[] args) {
        List<List<Integer>> lists;
        lists = new Solution77().combine(4, 2);
        System.out.println(lists);
    }
}
class Solution77 {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> lists = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        dfs(n, k, 0, list,lists);
        return lists;
    }
    public void dfs(int n, int k, int index, List<Integer> list,List<List<Integer>> lists) {
        if (k == 0) {
//            lists.add(list);//---有问题
            lists.add(new ArrayList<>(list));
            return ;
        }
//        for (int i = index; i < n; i++) {
        for (int i = index; i < n-k+1; i++) {//剪枝   剪枝前后,用时打败前50%和100%
            list.add(i + 1);//尝试添加
            dfs(n, k - 1, i + 1, list,lists);
            list.remove(list.size() - 1);//回溯,成功与否均进行回溯
        }
    }
}

4.subset https://leetcode-cn.com/problems/subsets/

import java.util.ArrayList;
import java.util.List;

public class No78_subSets {
    class Solution {
        List<List<Integer>> output = new ArrayList();
        int n, k;

        /**
         * 每一轮,与No46类似。不同之处在于,不要交换,因为交换后的1,2,3和1,3,2在这里看做是重复的
         * @param first
         * @param curr
         * @param nums
         */
        public void backtrack(int first, ArrayList<Integer> curr, int[] nums) {
            if (curr.size() == k)
                output.add(new ArrayList(curr));
            for (int i = first; i < n; ++i) {
                curr.add(nums[i]);
                backtrack(i + 1, curr, nums);
                curr.remove(curr.size() - 1);//回溯的目的是找到所有满足长度为k的组合
            }
        }

        public List<List<Integer>> subsets(int[] nums) {
            n = nums.length;
            // 【1,2,3】为例
            // 第一次是长度为0,空集
            // 第二次是长度为1,1或2或3.。。。
            for (k = 0; k < n + 1; ++k) {
                backtrack(0, new ArrayList<Integer>(), nums);
            }
            return output;
        }
    }
}