1. 程式人生 > 實用技巧 >LeetCode47. 全排列 II

LeetCode47. 全排列 II

思路1:回溯搜尋全排列,使用Set暴力去重。

☆☆☆思路2:回溯搜尋 + 剪枝。

  對原陣列排序,保證相同的數字都相鄰,然後每次填入的數一定是這個數所在重複數集合中「從左往右第一個未被填過的數字」

程式碼1:回溯搜尋 + Set去重

  程式碼1.1 ——交換位置確定數字(耗時:30ms)

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        
if (nums == null || nums.length == 0) return res; Set<List<Integer>> set = new HashSet<>(); dfs(nums, 0, set); res.addAll(set); return res; } private void dfs(int[] nums, int index, Set<List<Integer>> set) { if (index == nums.length) { List
<Integer> list = new ArrayList<>(); for (int num : nums) { list.add(num); } set.add(list); } for (int i = index; i < nums.length; i++) { swap(nums, index, i); dfs(nums, index + 1, set); swap(nums, index, i); } }
private void swap(int[] nums, int a, int b) { int temp = nums[a]; nums[a] = nums[b]; nums[b] = temp; } }

  程式碼1.2 ——設定標記陣列(耗時:40ms)

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        Set<List<Integer>> set = new HashSet<>();
        boolean[] visited = new boolean[nums.length];
        dfs(nums, visited, new ArrayList<>(), set);
        List<List<Integer>> res = new ArrayList<>(set);
        return res;
    }
    private void dfs(int[] nums, boolean[] visited, List<Integer> list, Set<List<Integer>> set) {
        if (list.size() == nums.length) {
            set.add(new ArrayList<>(list));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (visited[i]) continue;
            visited[i] = true;
            list.add(nums[i]);
            dfs(nums, visited, list, set);
            visited[i] = false;
            list.remove(list.size() - 1); // 注意remove傳入的引數是index
        }
    }
}

程式碼2:回溯搜尋 + 剪枝

  程式碼2.1——交換位置確定數字(耗時:1ms)

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        dfs(nums, 0, res);
        return res;
    }
    private void dfs(int[] nums, int index, List<List<Integer>> res) {
        if (index == nums.length) {
            List<Integer> list = new ArrayList<>();
            for (int num : nums) {
                list.add(num);
            }
            res.add(list);
            return;
        }
        for (int i = index; i < nums.length; i++) {
            // 搜尋前 先判斷是否已經被選過
            if (canSwap(nums, index, i)) {
                swap(nums, index, i);
                dfs(nums, index + 1, res);
                swap(nums, index, i);
            }
        }
    }
    /**
     *  如果當前準備選的下標是cur,而在index至cur-1中出現過相同的數字,
     *  說明數字肯定已經選過了。
     */
    private boolean canSwap(int[] nums, int start, int end) {
        for (int k = start; k < end; k++) {
            if (nums[k] == nums[end]) {
                return false;
            }
        }
        return true;
    }
    private void swap(int[] nums, int a, int b) {
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }
}

  程式碼2.2 ——設定標記陣列(耗時:1ms)

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        boolean[] visited = new boolean[nums.length];
        Arrays.sort(nums);  // 排序保證相同數字都相鄰
        dfs(nums, 0, visited, new ArrayList<>(), res);
        return res;
    }
    private void dfs(int[] nums, int index, boolean[] visited, List<Integer> list, List<List<Integer>> res) {
        if (index == nums.length) {
            res.add(new ArrayList<>(list));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (visited[i]) continue;
            // 排序保證了相同數字都相鄰,每次填入的數是這個數所在重複數集合中「從左往右第一個未被填過的數字」
            // 對 !visited[i-1] 的理解很關鍵
            if (i > 0 && nums[i] == nums[i - 1] && !visited[i-1]) continue;

            visited[i] = true;
            list.add(nums[i]);
            dfs(nums, index + 1, visited, list, res);
            visited[i] = false;
            list.remove(list.size() - 1);
        }
    }
}

難點: !vis[i - 1] (前一個元素還未使用過)的理解

  如果前一個相同元素未被使用過,則不使用當前元素。那麼每次填入的數一定是這個數所在重複集合中最左邊那個。

  當前值等於前一個值,有兩種情況:

    1. nums[i-1] 沒用過,為false。 說明回溯到了同一層,此時接著用nums[i]會與nums[i-1]重複。

    2. nums[i-1] 用過了,為true。 說明此時在nums[i-1]的下一層,相等不會重複。

用 !vis[i-1]是判斷填入perm同一個位置時,這個數是否被使用過,如果是false代表填入過(因為回溯時被撤銷標記了)

  雖然使用vis[i-1]也能AC,但!vis[i-1]更高效。