77. 組合
技術標籤:LeetCode
77. 組合
題目描述
給定兩個整數 n 和 k,返回 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(n∗2n)
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~n
這 n
個數每個數的狀態, 1
表示該數被選擇, 0
表示該數未被選。那麼題目就變成在長度為 n
的二進位制數中,找出所有二進位制表示中有 k
位為 1
的二進位制數。這樣我們就沒必要去考慮那些無用的狀態(1
的個數不等於 k
)。
這裡我們從最小的數開始,從小到大來考慮:
比如 n = 6
,k = 4
,題目變成從 x = 001111
開始,不停的查詢最小的比 x
大且二進位制表示中 1
的個數與 x
相同的數。
假設 N = 78
,二進位制表示為 1001110
,最小的比 N
大且二進位制表示中 1
與 N
相同的數,也就是 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
中最低位的 1
與 x
中的那個 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%
*/