1. 程式人生 > 實用技巧 >Leetcode 78 - 子集

Leetcode 78 - 子集

1 題目

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

2 題意

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

說明:解集不能包含重複的子集。

示例:

輸入: nums = [1,2,3]
輸出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

3 解題思路

author's blog == http://www.cnblogs.com/toulanboy/

(1)子集的概念

子集,簡單來說,就是將原集合的一部分元素抽出來形成的新集合。子集包含的元素個數可以是0~n

n: 原集合的長度

(2)如何看待子集的形成

由於子集是原集合的一部分元素,那麼對於每一個原集合的元素,都有可能被抽出或者不被抽出。
所以,我們可以給每一個原集合的元素用一個bit來標記其是否被抽出到新集合。

  • 0代表沒被抽出(沒被選中)
  • 1代表被抽出(被選中)

那麼,若原集合有n個元素,由於每個元素都有選或不選兩種可能,所以從排列組合的角度出發,原集合有2^n個子集。

(3)舉例學習

那麼對於題目的樣例{1, 2, 3}

從排列組合的角度來看,其一共有3個元素,每個元素都有選或不選的可能性,故共有2^3,即8個子集。

其子集和二進位制的關係如下表

子集 二進位制(為方便理解,部分有註釋) 十進位制
[] 000(3個都不選) 0
[3] 001(只選第3個) 1
[2] 010(只選第2個) 2
[2, 3] 011(選第2、第3) 3
[1] 100 4
[1, 3] 101 5
[1, 2] 110 6
[1, 2, 3] 111 7

通過分析這個例子,我們可以發現一個特性:

列舉子集的過程,實際上列舉二進位制從000加到111的過程,也就是從0加到7的過程。

該特性也可以從組合數學的角度出發分析:子集的每個元素都是選或不選,所以若原集合有n個元素,那麼n個bit組成的二進位制產生的所有情況都會被列舉到。換算成十進位制的角度,就是枚舉了0到2^n的過程。

(4)重要結論

  • 子集的列舉(以原集合只有3個元素為例子),實際上是列舉每一個元素的選或不選,等同於列舉3個bit組成的二進位制的所有情況。而這個所有情況,可以通過列舉從000加到111實現。
  • 000加到111的過程,可以是簡單的二進位制的加法。也可以是換算成10進位制進行加法。

官方題解是基於十進位制加法實現的,我的程式碼是基於二進位制加法實現的,好處是二進位制加法可以處理元素個數大於64的情況。

(5)知識補充

這裡主要是談談二進位制的加法。

最直接是方式是 從個位開始加,然後不斷進位,但是每次加法都需要遍歷整個陣列。

但實際上,通過觀察二進位制的加法過程,有一個更好的方法:

具體方案:對於一個二進位制,當需要進行加1時,只需從右往左找到第1個0,將這個0變為1,將這個0後面的1變為0。

4 程式碼實現

//author's blog == http://www.cnblogs.com/toulanboy/
class Solution {
public:
  //add_one() : 對二進位制進行加1
  //對於一個二進位制,當需要進行加1時,只需從右往左找到第1個0,將這個0變為1,將這個0後面的1變為0。
    void add_one(int * binary, int n){
        for(int i=n-1; i>=0; i--){
            if(binary[i] == 0){
                binary[i] = 1;
                break;
            }
            else{
                binary[i] = 0;
            }
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        int n = nums.size();
        //建立長度為nums.size(),用來表示原集合的n個元素的選擇情況。(思路中涉及到的二進位制陣列)
        int binary[n];
        memset(binary, 0, sizeof(binary));
        
        int times = pow(2, n);
        vector<vector<int>> ans;//儲存所有子集
        vector<int> sub_set; //儲存一個子集
        while(times--){//一共有2^n個子集
            sub_set.clear();
            for(int i=0; i<n; ++i){//列舉選擇情況
                if(binary[i] == 1){
                    sub_set.push_back(nums[i]);
                }
            }
            ans.push_back(sub_set);
            //讓二進位制從0000累加到1111
            //十進位制的角度:從0加到2^n-1
            add_one(binary, n);//二進位制加1
        }
        return ans;
    }
};