Difficulty: Medium, Asked-in: Amazon, Adobe.
Key takeaway: An excellent problem to learn problem-solving using backtracking and combinatorics. We can use similar ideas to solve other coding questions.
Given two numbers n and K and you have to find all possible combinations of K numbers from 1 to n.
Input: n = 4, K = 2
Output: [ [1 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4] ]
Explanation: We need to select all possible combinations of K numbers from the given n number, which is equal to C(n, K). If n = 4 and K = 2, then value of C(4, 2) = 4!/(2! * 2!) = 6.
Input: n = 5, K = 3
Output: [ [1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], [2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5] ].
Explanation: The value of C(5, 3) = 5!/(2! * 3!) = 10.
Important note: Before moving on to the solutions, we recommend trying this problem on paper for atleast 15 or 30 minutes. Enjoy problem-solving!
If we observe the output pattern of K combinations, there are two possibilities for every number from 1 to n: Either we include the number in the combinations or exclude it from the combination. For example, suppose n = 4 and K = 2. Let's generate combinations of size 2 (refer to the following diagram).
The idea for this approach comes from Pascal's Identity, i.e., C(n, K) = C(n-1, K) + C(n-1, K-1). Here:
To implement this, we consider both cases and recursively create all possible combinations. Suppose we use a function kCombination(output, temp, index, n, i, K), where initial call is kCombination(output, temp, 0, n, 1, K).
Parameters:
void kCombination(vector<vector<int>>& output, vector<int>& temp,
int index, int n, int i, int K)
{
if (index == K)
{
output.push_back(temp);
return;
}
if (i > n)
return;
// Include the current element
temp[index] = i;
kCombination(output, temp, index + 1, n, i + 1, K);
// Exclude the current element
kCombination(output, temp, index, n, i + 1, K);
}
vector<vector<int>> findKCombination(int K, int n)
{
vector<vector<int>> output;
// Temporary vector to store the combination
vector<int> temp(K, 0);
kCombination(output, temp, 0, n, 1, K);
return output;
}
This is a slightly different implementation of the above idea.
void kCombination(vector<vector<int>>& output,
vector<int>& temp, int i, int n, int K)
{
if (temp.size() == K)
{
output.push_back(temp);
return;
}
if (i > n)
return;
// Include the current element
temp.push_back(i);
kCombination(output, temp, i + 1, n, K);
// Exclude the current element
temp.pop_back();
kCombination(output, temp, i + 1, n, K);
}
vector<vector<int>> findKCombination(int K, int n)
{
vector<vector<int>> output;
// Temporary vector to store the combination
vector<int> temp;
kCombination(output, temp, 1, n, K);
return output;
}
Here each recursive call will generate two additional recursive calls, and this process continues until the construction of any one of the K combinations or the value of i exceeds n (base cases). On the other side, at each level of the recursion tree, one element is added to the combination. So the maximum depth of the recursion tree is n, and the total number of nodes is proportional to 2^n. Time complexity = O(2^n).
However, this is just a rough estimate, because not all root-to-leaf paths in the recursion tree will have a length of n. The key idea is that during the recursion when the size of the combination reaches K, the algorithm will push the current combination into the output and backtrack from there. This means the algorithm prunes the recursion based on the value of K and effectively explores the C(n, K) valid combinations.
Here output array is part of the problem because we need to return all possible K combinations. So we should not consider output as a part of the space complexity.
So overall space complexity = O(K) + O(n) = O(K + n).
The idea is to generate a combination tree where we fix each number from 1 to n and recursively build combinations of K numbers. Suppose we have n = 5 and K = 3.
Overall, all K combinations will start from some number. So we first fix the current number and recursively generate all the K combinations starting from that number. After this, we backtrack and move to the next number and do the same thing.
Suppose we are using the function kCombination(output, temp, index, start, end, K) to generate all K combinations, where the initial function call will be kCombination(output, temp, 0, 1, n, K).
Parameters:
void kCombination(vector<vector<int>>& output, vector<int>& temp,
int index, int start, int end, int K)
{
if (index == K)
{
output.push_back(temp);
return;
}
for (int i = start; i <= end && end - i + 1 >= K - index; i++)
{
temp[index] = i;
kCombination(output, temp, index + 1, i + 1, end, K);
}
}
vector<vector<int>> findKCombination(int K, int n)
{
vector<vector<int>> output;
vector<int> temp(K, 0);
kCombination(output, temp, 0, 1, n, K);
return output;
}
Here we are generating all combinations of size K that can be formed from n elements. So the total number of recursive calls will be approximately nCK (n choose K). So the time complexity of this code is O(nCK).
Please write in the message below if you find anything incorrect, or you want to share more insight, or you know some different approaches to solve this problem. Enjoy learning, Enjoy algorithms!