1. 程式人生 > 實用技巧 >【演算法刷題】全排列 II

【演算法刷題】全排列 II

本文為個人解題思路整理,水平有限,有問題歡迎交流


概覽

這題想做出來其實很簡單,但是可以通過剪枝不斷的優化效能,又是一道表面深搜實則優化的題

難度:中等

核心知識點:DFS(回溯) + 資料結構


題目來源

力扣:https://leetcode-cn.com/problems/permutations-ii/


題目要求

給定一個可包含重複數字的序列,返回所有不重複的全排列。


樣例

輸入1

[1,1,2]

輸出1

[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

解題思路

  • 第一想法,暴力深搜+回溯,按順序搜尋每個位置的每個可能數字,但是如何保證不重複呢

  • 不重複第一想法是狀態壓縮,然後存進圖,但是明顯不可取,因為沒有限制數字的大小

  • 繼續考慮如何去重複,得到一個思路:在一個排列中,兩個相等的數字是可以互換的,對吧

    舉個例子,存在兩個數字a和b相等,那麼排列...a...b......b...a...是完全一樣的對吧,也就是兩個序列重複了

    那麼,如果給出的序列中,a在b前面,...a...b...是合法的,...b...a...是不合法的,因為重複了

    再換個角度想一下,其實...b...如果不使用a是合法的,但...b...a...就不合法了,其實關鍵在於不能使用a

    現在問題就簡單了,只需要檢查a的存在即可,即:對於數字b,是否存在一個數字a,滿足條件:a在b之前,且a和b相等,且a未被使用

    但是要注意,如果a存在,不合法的其實是b,因為全排列必須使用所有數字,也就是必須使用a,也就是現在的b後面無論怎麼排都是不合法的

    總結一下就是,在使用數字b時,檢查b前面是否存在與b相等,且未被使用的數字

  • 顯然可以用map或者set來檢查數字是否被使用,檢查相等直接往前搜尋即可,簡單暴力


現在基本的解題思路定下來了,開始優化

  • 基本思路

    • 暴力深搜+回溯,按順序搜尋每個位置的每個可能數字
    • 搜尋某個位置的可能數字b時,確認這個數字在前面是否存在相等且未被使用的數字a,若有,那麼這個數字b不可用
  • 優化:因為深搜一個可能之後要回溯,那麼使用操作最後一個元素會方便的多(稍微比list方便,但其實也沒多少)

  • 優化:仔細想一下,其實這個序列的初識順序並沒有意義,反正是要考慮所有的排列,那麼如果在一開始就將序列進行排序(遞增或遞減),那麼就可以將相等的數字聚集到一起,往前搜尋與自己相等的數字會快很多很多

    比如原本是 1,3,4,5,1,2,考慮第二個1的時候,要往前找4位找到與自己相等的

    排序之後是1,1,2,3,4,5只需要往前找一位即可,如果出現與自己不相等的,就證明沒有與自己相等的了,可以提前結束檢索了

    要注意條件是與自己相等且未被使用哦,不只是相等而已


這個時候得到的方案基本如下

  • 對序列進行排序(遞增遞減均可)
  • 從第一個位置開始暴力深搜,使用回溯挨個嘗試序列的每個數字
  • 嘗試數字的時候,檢查這個數字前面是否有相等的數字,且未被使用,若有,則放棄當前這個數字(因為會重複)
  • 使用map記錄被使用過的數字在序列中的位置

一開始我的做法便是如此,但效能一般(5ms),看了題解發現可以進一步優化


  • 優化:對於序列a,b,c,d,e,如果a,b,c相等,那麼對於排列...a...b...c...,這三個數互相換位置都是同一個排列

    那麼固定這三個數的相對順序,不允許其他相對順序出現,那麼就能排除掉重複的排列

    不妨指定順序為序列中的順序,即對於a,b,c滿足

    • 不允許b出現在a之前
    • 不允許c出現在a和b之前

    又因為全排列必須使用所有數字,那麼等同於

    • 使用b之前必須已使用a
    • 使用c之前必須已使用b

    將這兩個條件通用化即,使用數字x之前滿足以下條件之一

    • x的前面的數字與x不相等
    • x前面的數字已被使用

優化完成,開始提出解決方案


解題方案

  1. 對序列進行排序(遞增遞減均可)
  2. 從排列的第1位開始暴力深搜
    1. 檢查排列的長度是否與序列相等,若是,則已搜尋到結果,儲存結果並返回上一層
    2. 從序列的第1個數字開始嘗試
    3. 檢查數字x是否滿足下面全部條件,若是則證明不可使用,嘗試序列的下一個數字
      • 前面有數字
      • x前面數字未被使用
      • x前面數字與x相等
    4. 將x使用次數加1
    5. 將排列中的第1位設定為x
    6. 繼續對排列第2位暴力深搜
    7. 將x的使用次數減1
    8. 繼續嘗試序列的下一個數字

完整程式碼

package com.company;

import java.util.*;

/**
 * @author Echo_Ye
 * @title 力扣47. 全排列 II
 * @description 排列求解
 * @date 2020/9/18 17:26
 * @email [email protected]
 */
public class PermuteUnique {
    public static void main(String[] args) {
        PermuteUnique permuteUnique = new PermuteUnique();
    }

    public PermuteUnique() {
        int[] nums = new int[]{1, 3, 1, 2};
        List<List<Integer>> ans = permuteUnique(nums);
        for (List<Integer> list : ans) {
            for (Integer i : list) {
                System.out.print(i + "    ");
            }
            System.out.println();
        }
    }

    //用於標記是否已被使用
    boolean[] isUsed;
    List<List<Integer>> ans = new ArrayList<>();

    public List<List<Integer>> permuteUnique(int[] nums) {
        //初始化之後預設全部為false
        isUsed = new boolean[nums.length];
        //排序
        Arrays.sort(nums);
        //開始遞迴搜尋
        dfs(new ArrayDeque<>(), nums, nums.length, 0);
        return ans;
    }

    /**
     * 遞迴深搜
     *
     * @param deque  當前排列
     * @param source 資料來源
     * @param total  總共資料數量
     * @param cur    當前檢索位置
     */
    public void dfs(Deque<Integer> deque, int[] source, int total, int cur) {
        if (cur == total) {
            //儲存答案
            ans.add(new ArrayList<>(deque));
        }
        for (int i = 0; i < total; i++) {
            //檢查i是否可用
            if (isUsed[i] || (i > 0 && !isUsed[i - 1] && source[i] == source[i - 1])) {
                continue;
            }
            //標記i為已用,且將其新增到排列
            isUsed[i] = true;
            deque.addLast(source[i]);
            //繼續下一層dfs
            dfs(deque, source, total, cur + 1);
            //回溯,標記i未用,且將其從排列末尾移除
            isUsed[i] = false;
            deque.removeLast();
        }
    }
}

結果

效能


後記

優勢典型的暴力搜尋加剪枝,重點在於後面一步一步的優化,我最開始的方案執行用時是5ms,看了題解優化後達到2ms,優化空間還是挺大的



作者:Echo_Ye

WX:Echo_YeZ

Email :[email protected]

個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)