1. 程式人生 > 其它 >77. 組合

77. 組合

技術標籤:LeetCode

77. 組合

題目描述

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

示例:

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

題解:

法一:

回溯 + 剪枝。

dfs( pos, rest, n ) ,表示當前從 pos 位置開始,還剩 rest 個元素待選擇,n 表示可選元素的範圍。

剪枝:如果 pos + rest > n ,說明此時把 pos~n-1 的元素都選擇了,還不夠 rest ,可以直接返回( pos

0 開始,既代表位置,也代表值)。

時間複雜度: O ( ( n k ) × k ) O(\binom{n}{k} \times k) O((kn)×k)

額外空間複雜度: O ( n + k ) O(n + k) O(n+k)

寫法有兩種,一種是迴圈寫法:

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    void dfs( int p, int rest, int n ) {
        if ( !rest )
{ ret.push_back( path ); return; } for ( int i = p; i + rest <= n; ++i ) { path.push_back( i + 1 ); dfs( i + 1, rest - 1, n ); path.pop_back(); } } vector<vector<int>> combine(int n, int k) { if
( k > n ) return {}; dfs( 0, k, n ); return ret; } }; /* 時間:4ms,擊敗:99.89% 記憶體:9.6MB,擊敗:74.38% */

另外一種寫法就是:選 與 不選,兩次 dfs

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    void dfs( int p, int rest, int n ) {
        if ( !rest ) {
            ret.push_back( path );
            return;
        }
        if( p >= n || p + rest > n ) return;
        
        path.push_back( p + 1 );
        dfs( p + 1, rest - 1, n );
        path.pop_back();

        dfs( p + 1, rest, n );
    }
    vector<vector<int>> combine(int n, int k) {
        if ( k > n ) return {};
        dfs( 0, k, n );
        return ret;
    }
};
/*
時間:8ms,擊敗:98.80%
記憶體:18.3MB,擊敗:18.46%
*/
法二:

考慮二進位制列舉子集,如果列舉所有狀態,時間複雜: O ( n ∗ 2 n ) O(n * 2^n) O(n2n)

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    vector<vector<int>> combine(int n, int k) {
        if ( k > n ) return {};
        for ( int i = (1 << k) - 1; i < (1 << n); ++i ) {
            path.clear();
            for ( int j = 0; j < n; ++j ) {
                if ( i >> j & 1 ) {
                    path.emplace_back( j + 1 );
                    if ( path.size() > k ) break;
                }
            }
            if ( path.size() == k ) ret.emplace_back( path );
        }
        return ret;
    }
};
/*
時間:160ms,擊敗:10.19%
記憶體:9.5MB,擊敗:74.82%
*/

其實,這種無腦列舉中有很多狀態我們不需要考慮。。。

換個角度考慮:假設我們用長度為 n 的二進位制數表示 1~nn 個數每個數的狀態, 1 表示該數被選擇, 0 表示該數未被選。那麼題目就變成在長度為 n 的二進位制數中,找出所有二進位制表示中有 k 位為 1 的二進位制數。這樣我們就沒必要去考慮那些無用的狀態(1 的個數不等於 k )。

這裡我們從最小的數開始,從小到大來考慮:

比如 n = 6k = 4,題目變成從 x = 001111 開始,不停的查詢最小的比 x 大且二進位制表示中 1 的個數與 x 相同的數。

假設 N = 78,二進位制表示為 1001110,最小的比 N 大且二進位制表示中 1N 相同的數,也就是 83,其二進位制表示為 1010011

我們來觀察 78 變成 83 的規律:

78: 1 0 0 1 1 1 0
83: 1 0 1 0 0 1 1

只需要對 78 的二進位制表示中最右邊連續的 1 串進行操作!

具體來說就是:將最右邊連續的 1 串中最左邊的 1 向左 移動 一位,其它的 1 移動 到最右邊。

這樣既可以保證二進位制表示中 1 的個數保持不變,且保證了新得到的數比原來的數大,且是最小的:

在這裡插入圖片描述

但具體操作時,不是對這些二進位制位進行移動,而是通過位操作來達到同樣的目的。

首先是 int x = N & (-N),找到 N 的二進位制表示中最右邊的這個 1(這個 1 必定是二進位制表示中最右邊連續的 1 串的開始)。

在這裡插入圖片描述

接下來考慮 int t = N + x,該語句將連續的 1 串中最左邊的 1 向左移動一位.但是這操作也讓連續的 1 串中其它的 1 丟失了。

在這裡插入圖片描述

接下來就是考慮將丟失的 1 補上,並移動到最右邊。

首先,需要知道需要補多少個 1,由上面可知,需要補得 1 的個數為 N 的二進位制表示中最右邊的連續的 1 串中 1 的個數減一。如何通過位操作來得到呢?可以通過 N^t 得到,N^t 的二進位制表示只包含 一個連續的 1,並且 1 的個數正好等於 N 的二進位制表示中最右邊連續的 1 串中 1 的個數加一:

在這裡插入圖片描述

所以 N^t 中的 1 的個數比我們需要補得 1 的個數多 2,並且 N^t 中最低位的 1x 中的那個 1 對應,這樣我們通過:((N^t)/x)>>2,得到要補的 1 的個數和其位置:

在這裡插入圖片描述

最後,t | ((N^t)/x) >> 2 就可以得到所求的數字了。

在這裡插入圖片描述

這種寫法還需要遍歷 N 的二進位制表示中每一位 1 的位置,預處理一下就行。

注意:二進位制變化的上界是 11110000......

時間複雜度: O ( ( n k ) × k ) O(\binom{n}{k} \times k) O((kn)×k)

額外空間複雜度: O ( n + k ) O(n + k) O(n+k)

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    vector<vector<int>> combine(int n, int k) {
        if ( k > n ) return {};
        int gap = (1 << n) - (1 << (n - k));
        int now = (1 << k) - 1;
        unordered_map<int, int> one_pos;
        path = vector<int>(k);
        for ( int i = 0; i < n; ++i ) one_pos[1 << i] = i;
        int idx = 0, lowbit;
        while ( now <= gap ) {
            int tmp = now;
            idx = 0;
            while ( tmp ) {
                lowbit = tmp & -tmp;
                path[idx++] = one_pos[lowbit] + 1;
                tmp ^= lowbit;
            }
            ret.emplace_back( path );
            int x = now & -now;
            int t = now + x;
            now = ((now ^ t) >> one_pos[x] >> 2) | t;
        }
        return ret;
    }
};
/*
時間:8ms,擊敗:98.80%
記憶體:9.7MB,擊敗:71.34%
*/