1. 程式人生 > 其它 >【LeetCode】全排列、子集、組合問題

【LeetCode】全排列、子集、組合問題

技術標籤:資料結構java集合

文章目錄


一、全排列

1.1 無重複元素全排列

LeetCode全排列

給定一個沒有重複數字的序列,返回其所有可能的全排列

//示例   輸入[1, 2, 3]
/*輸出    
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
*/
1.1.1 從左向右交換

觀察元組 [1, 2, 3],以元素1為第一位的排列有 [1, 2, 3][1, 3, 2],此排列可以由初始排列和交換2和3後得到。以元素2為第一位的元素排列有 [2, 1, 3][2, 3, 1] ,同樣以元素3為第一位的排列有 [3, 2, 1] (此處是321而不是312的原因是按下面程式碼執行順序來的,因為先交換了3和1,不同的交換順序或者操作不同生成的全排列順序不同,但其組成的集合是相同的,其他的順序下面會講到

) 和 [3, 1, 2]

在函式backtrack()中,迴圈中i初始值為begin是因為要將自己與自己交換來儲存當前這個序列,之後深度搜索遞迴處理交換位置從begin+1開始,遞迴結束後恢復現場,將交換的數交換回來,繼續迴圈交換處理第i+1個數據。

class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> permute(int[] nums) {
        res = new ArrayList();
        List<Integer>
temp = new ArrayList<>(); for(int i : nums){ temp.add(i); } backtrack(temp, 0, nums.length); return res; } public void backtrack(List<Integer> temp, int begin, int end){ if(begin == end){ res.add(new ArrayList<>(temp)); return; } for(int i = begin; i < end; i++){ Collections.swap(temp, begin, i); backtrack(temp, begin + 1, end); Collections.swap(temp, begin, i); } } }

在主函式中呼叫如下

Solution so = new Solution();
List list = so.permute(new int[] {1, 2, 3});
for(Object temp : list) {
	System.out.println(temp);
}

輸出結果如下圖所示,可看出第一個元素依次是1, 2, 3
在這裡插入圖片描述

1.1.2 從右往左交換

若將for迴圈修改為從end至begin交換

//backtrack(temp, 0, nums.length - 1);
public void backtrack(List<Integer> temp, int begin, int end){
    if(begin == end){
        res.add(new ArrayList<>(temp));
        return;
    }
    for(int i = end; i >= begin; i--){
        Collections.swap(temp, i, end);
        backtrack(temp, begin, end - 1);
        Collections.swap(temp, i, end);
    }
}

輸入結果如下圖所示,可看出最後一個元素依次是3,2,1

在這裡插入圖片描述

1.1.3 移動元素至左(升序排列)

若將第i個元素移動至begin前面,返回的序列正好是按其升序排列的序列,可對比從左向右交換的方法

for(int i = begin; i < end; i++){
    int t = temp.remove(i);
    temp.add(begin, t);
    backtrack(temp, begin + 1, end);
    temp.remove(begin);
    temp.add(i, t);
}

輸入結果如下圖所示,可看出其結果按升序排列

在這裡插入圖片描述

1.1.4 移動元素至右(逆序看降序排列)

若將第i個元素移動至end後面,返回的正好是按其序列逆序降序排列的序列,可以對比從右往左交換的方法

for(int i = end; i >= begin; i--){
    int t = temp.remove(i);
    temp.add(end, t);
    backtrack(temp, begin, end - 1);
    temp.remove(end);
    temp.add(i, t); 
}

輸入結果如下圖所示

在這裡插入圖片描述

【拓展】康託展開

LeetCode排列序列

輸入n和k,返回1-n按其升序排列的第k個排列序列

按大小順序列出所有排列情況,並一一標記,當 n = 3 時, 所有排列如下:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"

當k = 4時應輸出“231”

解題方法:此問題可以用上面講到的第三種方法找到第k個後終止搜尋並返回結果就可以了,但比較耗時。可以用下面的數學方法康託展開來解這道題。

康託展開是一個全排列到一個自然數的雙射,常用於構建雜湊表時的時間壓縮。康拓展開的實質是計算當前排列在所有由小到大全排列中的順序,因此是可逆的
X = a n ( n − 1 ) ! + a ( n − 1 ) ( n − 2 ) ! + ⋯ + a 1 0 ! X= a_n (n-1)!+a_(n-1) (n-2)!+⋯+a_1 0! X=an(n1)!+a(n1)(n2)!++a10!
其中,ai為整數,並且0<=ai<i, 1< =i <= n,ai表示原數的第i位在當前未出現的元素中是排在第幾個,詳細見連結

康託展開搜狗百科

class Solution {
    public String getPermutation(int n, int k) {
        int[] factor = new int[n];
        factor[0] = 1;
        for(int i = 1; i < n; i++) {
            factor[i] = factor[i - 1] * i;
        }
        ArrayList<Integer> list = new ArrayList<>();
        for(int i = 1; i <= n; i++) {
            list.add(i);
        }
        k--;
        StringBuffer sb = new StringBuffer();
        for(int i = n - 1; i >= 0; i--) {
            int t = k / factor[i];
            sb.append(list.remove(t));
            k %= factor[i];
        }
        return sb.toString();
    }
}

1.2 有重複元素全排列

LeetCode全排列II

給定一個可包含重複數字的序列 nums按任意順序 返回所有不重複的全排列

/*輸入:nums = [1,1,2]
輸出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]
 */

將此陣列元素加入ArrayList中,在每次遍歷元素前判斷此元素之前是否已經被交換過,若之前有相同的元素已經交換了,則跳過此元素。此過程可以用HashSet來實現。就是在上述1.1從左向右交換中使用HashSet,具體程式碼如下

public void backtrack(List<Integer> list, int begin, int end){
    if(begin == end){
        res.add(new ArrayList(list));
        return;
    }
    Set<Integer> set = new HashSet<>();
    for(int i = begin; i < end; i++){
        if(set.contains(list.get(i))){
            continue;
        }
        set.add(list.get(i));
        Collections.swap(list, begin, i);
        backtrack(list, begin + 1, end);
        Collections.swap(list, begin, i);
    }
}

對於陣列[1, 1, 2, 2],程式執行結果如下圖所示
在這裡插入圖片描述


二、子集

2.1 無重複元素子集

LeetCode子集

給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)

【注意】 解集不能包含重複的子集

//輸入 nums = [1, 2, 3]
/*輸出
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
*/
2.1.1 回溯法

空集必定包含在子集中,可以初始化list為空,所以backtrack()函式開始時應在結果集中加入list,然後從begin開始每次遍歷一個元素將其加入到list中,深度遍歷的下一步是遍歷第i+1個元素(因為已經加入了第i個元素,此時遞迴加入第i+1個元素),最後一步狀態重置移除剛剛加入的第i個元素。然後繼續迴圈處理第i+1個元素。此處使用LinkedList方便新增和刪除

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, new LinkedList<Integer>(), 0);
        return res;
    }
    public void backtrack(int[] nums, LinkedList<Integer> list, int begin){
        res.add(new LinkedList<Integer>(list));
        for(int i = begin; i < nums.length; i++) {
        	list.add(nums[i]);
        	backtrack(nums, list, i + 1);
        	list.removeLast();
        }
    }
}

呼叫程式nums = [1, 2, 3]的輸出結果如下圖所示

在這裡插入圖片描述

2.1.2 二進位制計數法

對於 nums = [1, 2, 3]來說,有三個元素,則包含的子集個數為2^3 = 8個,此八個子集分別為0 - 7對應的二進位制中相應位置為1的元素組合,如下示例

/*
   321
0  000  []
1  001  [1]
2  010  [2]
3  011  [2, 1]
4  100  [3]
5  101  [3, 1]
6  110  [3, 2]
7  111  [3, 2, 1]
*/

所以可以遍歷0-7的數字,根據對應二進位制位的數字為1(或0)來決定加入(不加入)對應元素,具體程式碼如下

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int n = (1 << nums.length);
        for(int i = 0; i < n; i++){
            List<Integer> list = new ArrayList<>();
            int x = i;
            for(int j = 0; j < nums.length; j++){
                if((x & 1) == 1){
                    list.add(nums[j]);
                }
                x >>= 1;
            }
            res.add(list);
        }
        return res;
    }
}

輸出結果res如下圖所示

在這裡插入圖片描述

2.1.3 迭代法

可以先將空集加入結果集,然後每遍歷一個元素,將此元素加入結果集的每個子集中形成新的集合,將此集合加入到結果集中,直至遍歷完最後一個元素,詳細過程如下

/*
[]
[1]
[2]
[1, 2]
[3]
[1, 3]
[2, 3]
[1, 2, 3]
*/

具體程式碼如下

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<Integer>());
        for(int x : nums){
            int len = res.size();
            for(int i = 0; i < len; i++){
                List<Integer> list = new ArrayList(res.get(i));
                list.add(x);
                res.add(list);
            }
        }
        return res;
    }
}

程式輸出結果如下

在這裡插入圖片描述

2.2 有重複元素子集

LeetCode子集II

給定一個可能包含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)

【注意】解集不能包含重複的子集

/*
輸入: [1,2,2]
輸出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]
*/
2.2.1 回溯法

首先要進行排序,之後再用HashSet去重。若不進行排序,對於像[4, 4, 4, 1, 4]這樣的序列,因為每次回溯過程中會移除當前元素,假如在移除了第三個4後遍歷到最後一個4,會重複記錄[4, 4, 1, 4]。

對於有重複元素的全排列來說,不需要排序,因為它的集合是所有元素的組合

class Solution {
	List<List<Integer>> res;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        res = new ArrayList<>();
        backtrack(nums, new ArrayList<Integer>(), 0);
        return res;
    }
    private void backtrack(int[] nums, List<Integer> list, int begin) {
    	res.add(new ArrayList<Integer>(list));
    	Set<Integer> set = new HashSet<>();
    	for(int i = begin; i < nums.length; i++) {
    		if(set.contains(nums[i])) {
    			continue;
    		}
    		set.add(nums[i]);
    		list.add(nums[i]);
    		backtrack(nums, list, i + 1);
    		list.remove(list.size() - 1);
    	}
    }
}

對於陣列nums = [4, 4, 4, 1, 4]執行結果如下圖所示

在這裡插入圖片描述

2.2.2 迭代法

首先要進行排序

和無重複元素中的迭代法一樣,假如nums = [1, 2, 2, 3],根據前面的迭代若此時結果集中元組為[[], [1], [2], [1, 2]],且[2]和[1, 2]是遍歷第一個2時生成的,此時在遍歷第二個2時,對[]和[1]不需重新加入了,只需在遍歷前面2的生成的子集基礎上加入2,即得到 [2, 2][1, 2, 2] 。因此需要記錄每次新生成的子集數,若當前元素與前一個元素不相等,則結果集從0開始處理;若相同,則從(list.size() - count)處開始處理。具體見程式碼和執行結果分析。

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<Integer>());
        Arrays.sort(nums);
        int count = 0;
        for(int i = 0; i < nums.length; i++){
            int begin = 0;
            if(i != 0 && nums[i] == nums[i - 1]){
                begin = res.size() - count;
            }
            int len = res.size();
            count = 0;
            for(int j = begin; j < len; j++){
                List<Integer> list = new ArrayList<>(res.get(j));
                list.add(nums[i]);
                res.add(list);
                count++;
            }
        }
        return res;
    }
}

執行結果和分析如下

在這裡插入圖片描述


三、組合

3.1 組合

LeetCode組合

給定兩個整數 nk,返回 1 … n 中所有可能的 k 個數的組合

/*
輸入: n = 4, k = 2
輸出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
*/

組合其實就是在子集問題的基礎上增加了約束條件,使得子集中元素的長度為k。所以可以在選擇將集合加入結果集時判斷是否等於要求個數k,可以通過剪枝提高效率,若當前集合元素個數加上未遍歷元素個數小於k,剩餘元素沒必要判斷了。

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtrack(new LinkedList<Integer>(), 1, n, k);
        return res;
    }
    public void backtrack(LinkedList<Integer> list, int begin, int n, int k){
        if(n - begin + 1 < k){
            return;
        }
        if(k == 0){
            res.add(new LinkedList<>(list));
            return;
        }
        for(int i = begin; i <= n; i++){
            list.add(i);
            backtrack(list, i + 1, n, k - 1);
            list.removeLast();
        }
    }
}

對於示例 n = 4, k = 2 時執行程式輸出結果如下圖所示

在這裡插入圖片描述

3.2 組合總和

LeetCode組合總和

給定一個無重複元素的陣列 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。

candidates 中的數字可以無限制重複被選取

說明

所有數字(包括 target)都是正整數
解集不能包含重複的組合

/*輸入:candidates = [2,3,6,7], target = 7,
所求解集為:
[
  [7],
  [2,2,3]
]
*/

因為陣列中的數字可以重複選取,所以在遞迴呼叫backtrack函式時begin是從i開始,表示還是從當前元素判斷,直到target < 0(即目標和大於給定初始值target)為止。具體程式碼如下所示(此處只給出backtrack( )函式)

public void backtrack(int[] candidates, LinkedList<Integer> list ,int begin, int end, int target){
    if(target < 0){
        return;
    }
    if(target == 0){
        res.add(new LinkedList(list));
        return;
    }
    for(int i = begin; i < end; i++){
        list.add(candidates[i]);
        backtrack(candidates, list, i, end, target - candidates[i]);
        list.removeLast();
    }
}

對於 candidates = [1, 2, 3], target = 6應用程式輸出結果如下圖

在這裡插入圖片描述

3.3 組合總和IV

LeetCode組合總和IV

給定一個由正整陣列成且不存在重複數字的陣列,找出和為給定目標正整數的組合的個數

/*
nums = [1, 2, 3]
target = 4
輸出: 7
所有可能的組合為:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
請注意,順序不同的序列被視作不同的組合
*/

因為不同的順序被視為不同的組合,此問題是組合和全排列的結合,使用回溯法可以先求出和為目標數的子集(雜湊表去重排序後的子集),然後求子集的全排列個數,之後相加便是。

但這道題使用動態規劃可以很簡單地解決,相關程式碼如下

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for(int i = 1; i <= target; i++){
            for(int j : nums){
                if(i >= j){
                    dp[i] += dp[i - j];
                }
            }
        }
        return dp[target];
    }
}

四、總結

類似的題目有字母的全排列、字母的子集,組合總和II,組合總和III······

相信你只要認真讀過上述所有題解,下次遇到類似的題目一定會清晰快速地完成!