【演算法刷題】全排列 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位開始暴力深搜
- 檢查排列的長度是否與序列相等,若是,則已搜尋到結果,儲存結果並返回上一層
- 從序列的第1個數字開始嘗試
- 檢查數字x是否滿足下面全部條件,若是則證明不可使用,嘗試序列的下一個數字
- 前面有數字
- x前面數字未被使用
- x前面數字與x相等
- 將x使用次數加1
- 將排列中的第1位設定為x
- 繼續對排列第2位暴力深搜
- 將x的使用次數減1
- 繼續嘗試序列的下一個數字
完整程式碼
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]
個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)