Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Recursion - Understanding how to break problems into smaller subproblems with base cases
Backtracking - Making choices (include/exclude), exploring paths, and undoing choices
Bit Manipulation - Using bitmasks to represent subsets (for the bit manipulation approach)
Combinatorics Basics - Understanding what combinations are and how they differ from permutations
1. Backtracking - I
Intuition
To generate all combinations of k numbers from 1 to n, we make a binary choice for each number: include it or exclude it. This forms a decision tree where each path represents a subset. We only keep subsets of exactly size k.
Algorithm
Start with an empty combination and index i = 1.
At each step, we have two choices: include the current number i in the combination, or skip it.
Recursively process both choices by incrementing i.
When i exceeds n, check if the combination has exactly k elements. If so, add a copy to the result.
Backtrack by removing the added element before exploring the skip branch.
funccombine(n int, k int)[][]int{
res :=[][]int{}var backtrack func(i int, comb []int)
backtrack =func(i int, comb []int){if i > n {iflen(comb)== k {
temp :=make([]int,len(comb))copy(temp, comb)
res =append(res, temp)}return}
comb =append(comb, i)backtrack(i+1, comb)
comb = comb[:len(comb)-1]backtrack(i+1, comb)}backtrack(1,[]int{})return res
}
class Solution {funcombine(n: Int, k: Int): List<List<Int>>{val res = mutableListOf<List<Int>>()funbacktrack(i: Int, comb: MutableList<Int>){if(i > n){if(comb.size == k){
res.add(ArrayList(comb))}return}
comb.add(i)backtrack(i +1, comb)
comb.removeAt(comb.size -1)backtrack(i +1, comb)}backtrack(1,mutableListOf())return res
}}
classSolution{funccombine(_ n:Int,_ k:Int)->[[Int]]{var res =[[Int]]()funcbacktrack(_ i:Int,_ comb:inout[Int]){if i > n {if comb.count == k {
res.append(comb)}return}
comb.append(i)backtrack(i +1,&comb)
comb.removeLast()backtrack(i +1,&comb)}var comb =[Int]()backtrack(1,&comb)return res
}}
implSolution{pubfncombine(n:i32, k:i32)->Vec<Vec<i32>>{letmut res =Vec::new();fnbacktrack(i:i32, n:i32, k:i32, comb:&mutVec<i32>, res:&mutVec<Vec<i32>>){if i > n {if comb.len()== k asusize{
res.push(comb.clone());}return;}
comb.push(i);backtrack(i +1, n, k, comb, res);
comb.pop();backtrack(i +1, n, k, comb, res);}backtrack(1, n, k,&mutvec![],&mut res);
res
}}
Time & Space Complexity
Time complexity: O(k∗(n−k)!∗k!n!)
Space complexity: O(k∗(n−k)!∗k!n!) for the output array.
Where n is the number of elements and k is the number of elements to be picked.
2. Backtracking - II
Intuition
Instead of making include/exclude decisions, we iterate through available numbers and always include one. Starting from a given position ensures we never revisit smaller numbers, avoiding duplicates. We stop when the combination reaches size k.
Algorithm
Start with an empty combination and a starting index of 1.
If the combination size equals k, save a copy to the result and return.
Iterate from the start index to n. For each number, add it to the combination.
Recursively call with start = i + 1 to only consider larger numbers.
Remove the last element (backtrack) before trying the next number.
classSolution:defcombine(self, n:int, k:int)-> List[List[int]]:
res =[]defbacktrack(start, comb):iflen(comb)== k:
res.append(comb.copy())returnfor i inrange(start, n +1):
comb.append(i)
backtrack(i +1, comb)
comb.pop()
backtrack(1,[])return res
publicclassSolution{privateList<List<Integer>> res;publicList<List<Integer>>combine(int n,int k){
res =newArrayList<>();backtrack(1, n, k,newArrayList<>());return res;}privatevoidbacktrack(int start,int n,int k,List<Integer> comb){if(comb.size()== k){
res.add(newArrayList<>(comb));return;}for(int i = start; i <= n; i++){
comb.add(i);backtrack(i +1, n, k, comb);
comb.remove(comb.size()-1);}}}
classSolution{public:
vector<vector<int>> res;
vector<vector<int>>combine(int n,int k){
res.clear();
vector<int> comb;backtrack(1, n, k, comb);return res;}voidbacktrack(int start,int n,int k, vector<int>& comb){if(comb.size()== k){
res.push_back(comb);return;}for(int i = start; i <= n; i++){
comb.push_back(i);backtrack(i +1, n, k, comb);
comb.pop_back();}}};
classSolution{/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/combine(n, k){const res =[];constbacktrack=(start, comb)=>{if(comb.length === k){
res.push([...comb]);return;}for(let i = start; i <= n; i++){
comb.push(i);backtrack(i +1, comb);
comb.pop();}};backtrack(1,[]);return res;}}
publicclassSolution{publicList<List<int>>Combine(int n,int k){List<List<int>> res =newList<List<int>>();voidBacktrack(int start,List<int> comb){if(comb.Count == k){
res.Add(newList<int>(comb));return;}for(int i = start; i <= n; i++){
comb.Add(i);Backtrack(i +1, comb);
comb.RemoveAt(comb.Count -1);}}Backtrack(1,newList<int>());return res;}}
funccombine(n int, k int)[][]int{
res :=[][]int{}var backtrack func(start int, comb []int)
backtrack =func(start int, comb []int){iflen(comb)== k {
temp :=make([]int,len(comb))copy(temp, comb)
res =append(res, temp)return}for i := start; i <= n; i++{
comb =append(comb, i)backtrack(i+1, comb)
comb = comb[:len(comb)-1]}}backtrack(1,[]int{})return res
}
class Solution {funcombine(n: Int, k: Int): List<List<Int>>{val res = mutableListOf<List<Int>>()funbacktrack(start: Int, comb: MutableList<Int>){if(comb.size == k){
res.add(ArrayList(comb))return}for(i in start..n){
comb.add(i)backtrack(i +1, comb)
comb.removeAt(comb.size -1)}}backtrack(1,mutableListOf())return res
}}
classSolution{funccombine(_ n:Int,_ k:Int)->[[Int]]{var res =[[Int]]()funcbacktrack(_ start:Int,_ comb:inout[Int]){if comb.count == k {
res.append(comb)return}for i in start...n {
comb.append(i)backtrack(i +1,&comb)
comb.removeLast()}}var comb =[Int]()backtrack(1,&comb)return res
}}
implSolution{pubfncombine(n:i32, k:i32)->Vec<Vec<i32>>{letmut res =Vec::new();fnbacktrack(start:i32, n:i32, k:i32, comb:&mutVec<i32>, res:&mutVec<Vec<i32>>){if comb.len()== k asusize{
res.push(comb.clone());return;}for i in start..=n {
comb.push(i);backtrack(i +1, n, k, comb, res);
comb.pop();}}backtrack(1, n, k,&mutvec![],&mut res);
res
}}
Time & Space Complexity
Time complexity: O(k∗(n−k)!∗k!n!)
Space complexity: O(k∗(n−k)!∗k!n!) for the output array.
Where n is the number of elements and k is the number of elements to be picked.
3. Iteration
Intuition
We can simulate backtracking iteratively using an array of size k to track our current combination. An index pointer moves forward when we find valid numbers and backward when we need to backtrack. This eliminates recursion overhead.
Algorithm
Initialize an array of size k with zeros and a pointer i = 0.
Increment comb[i] to try the next value at position i.
If comb[i] exceeds n, move the pointer back (i -= 1) to backtrack.
If i reaches k - 1, we have a complete combination; save it.
Otherwise, move forward (i += 1) and set comb[i] = comb[i - 1] so the next position starts just after the previous value.
classSolution:defcombine(self, n:int, k:int)-> List[List[int]]:
res =[]
i =0
comb =[0]* k
while i >=0:
comb[i]+=1if comb[i]> n:
i -=1continueif i == k -1:
res.append(comb.copy())else:
i +=1
comb[i]= comb[i -1]return res
publicclassSolution{publicList<List<Integer>>combine(int n,int k){List<List<Integer>> res =newArrayList<>();int[] comb =newint[k];int i =0;while(i >=0){
comb[i]++;if(comb[i]> n){
i--;continue;}if(i == k -1){List<Integer> current =newArrayList<>();for(int num : comb){
current.add(num);}
res.add(current);}else{
i++;
comb[i]= comb[i -1];}}return res;}}
classSolution{/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/combine(n, k){const res =[];const comb =Array(k).fill(0);let i =0;while(i >=0){
comb[i]++;if(comb[i]> n){
i--;continue;}if(i === k -1){
res.push([...comb]);}else{
i++;
comb[i]= comb[i -1];}}return res;}}
publicclassSolution{publicList<List<int>>Combine(int n,int k){List<List<int>> res =newList<List<int>>();int[] comb =newint[k];int i =0;while(i >=0){
comb[i]++;if(comb[i]> n){
i--;continue;}if(i == k -1){
res.Add(newList<int>(comb));}else{
i++;
comb[i]= comb[i -1];}}return res;}}
funccombine(n int, k int)[][]int{
res :=[][]int{}
comb :=make([]int, k)
i :=0for i >=0{
comb[i]++if comb[i]> n {
i--continue}if i == k-1{
temp :=make([]int, k)copy(temp, comb)
res =append(res, temp)}else{
i++
comb[i]= comb[i-1]}}return res
}
class Solution {funcombine(n: Int, k: Int): List<List<Int>>{val res = mutableListOf<List<Int>>()val comb =IntArray(k)var i =0while(i >=0){
comb[i]++if(comb[i]> n){
i--continue}if(i == k -1){
res.add(comb.toList())}else{
i++
comb[i]= comb[i -1]}}return res
}}
classSolution{funccombine(_ n:Int,_ k:Int)->[[Int]]{var res =[[Int]]()var comb =[Int](repeating:0, count: k)var i =0while i >=0{
comb[i]+=1if comb[i]> n {
i -=1continue}if i == k -1{
res.append(comb)}else{
i +=1
comb[i]= comb[i -1]}}return res
}}
implSolution{pubfncombine(n:i32, k:i32)->Vec<Vec<i32>>{let k = k asusize;letmut res =Vec::new();letmut comb =vec![0i32; k];letmut i:i32=0;while i >=0{
comb[i asusize]+=1;if comb[i asusize]> n {
i -=1;continue;}if i asusize== k -1{
res.push(comb.clone());}else{let prev = comb[i asusize];
i +=1;
comb[i asusize]= prev;}}
res
}}
Time & Space Complexity
Time complexity: O(k∗(n−k)!∗k!n!)
Space complexity: O(k∗(n−k)!∗k!n!) for the output array.
Where n is the number of elements and k is the number of elements to be picked.
4. Bit Manipulation
Intuition
Each subset of numbers from 1 to n can be represented as an n-bit binary number, where bit i being set means number (i+1) is included. We iterate through all possible bitmasks and keep only those with exactly k bits set.
Algorithm
Iterate through all integers from 0 to 2^n - 1. Each integer represents a possible subset.
For each mask, extract the numbers corresponding to set bits (if bit j is set, include j + 1).
If the resulting subset has exactly k elements, add it to the result.
classSolution{/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/combine(n, k){const res =[];for(let mask =0; mask <1<< n; mask++){if(mask.toString(2).split('1').length -1!== k){continue;}const comb =[];for(let bit =0; bit < n; bit++){if(mask &(1<< bit)){
comb.push(bit +1);}}
res.push(comb);}return res;}}
publicclassSolution{publicList<List<int>>Combine(int n,int k){List<List<int>> res =newList<List<int>>();for(int mask =0; mask <(1<< n); mask++){List<int> comb =newList<int>();for(int bit =0; bit < n; bit++){if((mask &(1<< bit))!=0){
comb.Add(bit +1);}}if(comb.Count == k){
res.Add(comb);}}return res;}}
funccombine(n int, k int)[][]int{
res :=[][]int{}for mask :=0; mask <(1<< n); mask++{
comb :=[]int{}for bit :=0; bit < n; bit++{if mask&(1<<bit)!=0{
comb =append(comb, bit+1)}}iflen(comb)== k {
res =append(res, comb)}}return res
}
class Solution {funcombine(n: Int, k: Int): List<List<Int>>{val res = mutableListOf<List<Int>>()for(mask in0until(1shl n)){val comb = mutableListOf<Int>()for(bit in0 until n){if(mask and(1shl bit)!=0){
comb.add(bit +1)}}if(comb.size == k){
res.add(comb)}}return res
}}
classSolution{funccombine(_ n:Int,_ k:Int)->[[Int]]{var res =[[Int]]()for mask in0..<(1<< n){var comb =[Int]()for bit in0..<n {if mask &(1<< bit)!=0{
comb.append(bit +1)}}if comb.count == k {
res.append(comb)}}return res
}}
implSolution{pubfncombine(n:i32, k:i32)->Vec<Vec<i32>>{letmut res =Vec::new();for mask in0..(1<< n){letmut comb =Vec::new();for bit in0..n {if mask &(1<< bit)!=0{
comb.push(bit +1);}}if comb.len()== k asusize{
res.push(comb);}}
res
}}
Time & Space Complexity
Time complexity: O(n∗2n)
Space complexity: O(k∗(n−k)!∗k!n!) for the output array.
Where n is the number of elements and k is the number of elements to be picked.
Common Pitfalls
Forgetting to Copy the Combination Before Adding to Results
When adding the current combination to the result list, you must create a copy. Otherwise, subsequent modifications during backtracking will alter the already-added result.
# Wrong: adds reference that gets modified
res.append(comb)# Correct: adds a copy
res.append(comb.copy())
Missing the Backtrack Step
After exploring a branch with an element included, you must remove it before exploring the next branch. Forgetting to pop leads to combinations with too many elements.
Starting the loop at 0 instead of start causes duplicate combinations like [1,2] and [2,1]. The loop must start from the current position to ensure each combination is generated only once in sorted order.
Sign in to join the discussion