Combinations

Updated on 12 May, 2025
Combinations header image

Problem Statement

Given two integers, n and k, the task is to generate all possible combinations of k numbers chosen from the set {1, 2, ..., n}. Importantly, the order of numbers within each combination does not matter. For instance, if the selected numbers are from 1 to 4 and you are choosing 2 out of them, [2,1] is the same as [1,2] and should only be considered once. This problem asks for a deep understanding of combination calculations where only unique sets of chosen numbers are returned without any order significance. It provides a way to grasp the idea of combinators in mathematical terms and explore algorithms that can handle generating these combinations efficiently.

Examples

Example 1

Input:

n = 4, k = 2

Output:

[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]

Explanation:

There are 4 choose 2 = 6 total combinations.
Note that combinations are unordered, i.e., [1,2] and [2,1] are considered to be the same combination.

Example 2

Input:

n = 1, k = 1

Output:

[[1]]

Explanation:

There is 1 choose 1 = 1 total combination.

Constraints

  • 1 <= n <= 20
  • 1 <= k <= n

Approach and Intuition

The problem requires generating combinations, which is a fundamental task often solved using backtracking - a recursive strategy to build up solutions incrementally, removing solutions that fail to satisfy the constraints (in this case ensuring each solution is a unique combination of exactly k elements).

Given the nature of the problem, where the answer needs to include all possible unordered k-length subsets of a set that ranges from 1 to n, let's break down our approach:

  1. Start with an empty list to store the combinations.
  2. Use a helper function that will recursively:
    • Choose an element to add to the running combination.
    • Recurse with this new combination.
    • Backtrack by removing the recently added element and trying the next possible candidate.
  3. This recursive function will iterate over the sequence from the last included element's next to n, ensuring that each combination is generated in a sorted manner (thus inherently avoiding duplicates like [2,1] when [1,2] has already been considered).
  4. Once the length of the current combination reaches k, it's added to the list of completed combinations.
  5. The recursion base case triggers when the combination size equals k, ensuring all combinations are of the required size.

Using this recursive backtracking approach, we ensure that:

  • All combinations are explored.
  • Each combination is collected in a sorted order once.
  • Efficiency by backtracking early if a potential combination surpasses allowed sizes, thanks to the constraints of our problem.

Examples help in understanding the effectiveness of this method:

  • For n = 4 and k = 2, the function would sequentially build combinations by starting with [1], then adding [2], checking if full [1, 2] matches k = 2, and recursively processing in a similar fashion for [1, 3], [1, 4], and so on, for embeddings like [2, 3], [3, 4] without ever reusing numbers.
  • The enumeration is controlled and does not repeat due to the ordered additions and the base case check that halts deeper recursions when k size is achieved.

By managing the depth of recursion and ensuring selected numbers are only advanced forward, the method can effectively generate orderly combinations without redundancy. Thus, it cleverly navigates the exponential nature of combination problems within given constraints.

Solutions

  • C++
  • Java
  • C
  • JavaScript
  • Python
cpp
class Solution {
public:
    int comboLength;
    int setLimit;

    vector<vector<int>> generateCombinations(int setLimit, int comboLength) {
        this->comboLength = comboLength;
        this->setLimit = setLimit;
        vector<vector<int>> result;
        vector<int> combination;
        findCombinations(combination, 1, result);
        return result;
    }

    void findCombinations(vector<int>& combination, int start, vector<vector<int>>& result) {
        if (combination.size() == comboLength) {
            result.push_back(combination);
            return;
        }

        int needed = comboLength - combination.size();
        int left = setLimit - start + 1;
        int maxReach = left - needed;

        for (int value = start; value <= start + maxReach; value++) {
            combination.push_back(value);
            findCombinations(combination, value + 1, result);
            combination.pop_back();
        }
    }
};

The solution presented in C++ aims to generate all possible combinations of a given length from a set of numbers ranging from 1 to setLimit. The functions and logic used are designed to address the problem of generating combinations efficiently using recursion and backtracking. Here's a brief understanding of the method and structure used in the code:

  • Initialize class variables in generateCombinations:

    • comboLength to capture the length of each combination
    • setLimit to determine the upper limit of the number set from which combinations are generated.
  • Define base vectors:

    • result to store all possible combinations.
    • combination to keep the current combination being explored.
  • Definition and use of helper function findCombinations:

    • The function checks if the size of the combination vector matches the comboLength, pushing it to result upon match.
    • Variables needed and left help manage the limits and efficiently prune unnecessary recursive calls.
  • The for-loop in findCombinations:

    • Controls the iteration over potential numbers to be added to the current combination, ensuring optimization by limiting the recursive depth to possible and necessary candidates using maxReach.
  • Backtracking step using combination.pop_back() to undo the last action and explore new possibilities by recursive re-entry with the next sequential value.

Ultimately, the function generateCombinations returns a vector of vectors, with each nested vector representing a possible combination, allowing for deep exploration of combinations within specified constraints promptly and efficiently.

java
class Solution {
    private int totalNumbers;
    private int subsetSize;

    public List<List<Integer>> combine(int n, int k) {
        this.totalNumbers = n;
        this.subsetSize = k;
        List<List<Integer>> combinations = new ArrayList<>();
        generateCombinations(new ArrayList<>(), 1, combinations);
        return combinations;
    }

    public void generateCombinations(
        List<Integer> combination,
        int start,
        List<List<Integer>> combinations
    ) {
        if (combination.size() == subsetSize) {
            combinations.add(new ArrayList<>(combination));
            return;
        }

        int elementsNeeded = subsetSize - combination.size();
        int remaining = totalNumbers - start + 1;
        int maxPossibleStart = start + remaining - elementsNeeded;

        for (int value = start; value <= maxPossibleStart; value++) {
            combination.add(value);
            generateCombinations(combination, value + 1, combinations);
            combination.remove(combination.size() - 1);
        }
    }
}

The Combinations problem requires generating all possible combinations of k numbers from a set of numbers [1..n]. The provided Java solution employs a recursive approach to solve this problem. Below, understand the key components and flow of the solution:

  • Two private instance variables, totalNumbers and subsetSize, are declared to store the total numbers (n) and the size of each subset (k) respectively.

  • The combine method initializes the process:

    • It assigns n to totalNumbers and k to subsetSize.
    • An empty list combinations is created to store all the resulting subsets.
    • The generateCombinations method is called with an initially empty list combination, starting index 1, and the list combinations.
    • Finally, it returns the list combinations that now contains all the desired subsets.
  • The recursive method generateCombinations constructs combinations:

    • It checks if the current list combination has reached the desired size (subsetSize). If yes, it adds a copy of combination to combinations and returns.
    • It calculates elementsNeeded as the difference between subsetSize and the current size of combination.
    • remaining represents how many numbers are left to consider from the current start point.
    • maxPossibleStart calculates the farthest possible starting point for the loop to ensure that enough elements are left for a valid combination.
    • A loop starts at start and continues to maxPossibleStart. In each iteration:
      • Current value is added to combination.
      • Recursively calls itself with the next start position.
      • Removes the last added element to backtrack and try the next potential number.

This method efficiently explores only feasible combinations by restricting the starting point of the loop, thereby avoiding unnecessary computation. This elegant mechanism leverages both backtracking and pruning, typical in combinatorial generation tasks.

c
void recursive_combination(int total, int select_count, int* combination_set, int current_size, int start_value, int** combinations, int* combinations_count) {
    if (current_size == select_count) {
        combinations[*combinations_count] = malloc(select_count * sizeof(int));
        memcpy(combinations[*combinations_count], combination_set, select_count * sizeof(int));
        (*combinations_count)++;
        return;
    }
    int needed = select_count - current_size;
    int remaining = total - start_value + 1;
    int possible = remaining - needed;
    for (int i = start_value; i <= start_value + possible; i++) {
        combination_set[current_size] = i;
        recursive_combination(total, select_count, combination_set, current_size + 1, i + 1, combinations, combinations_count);
    }
}

int** get_combinations(int n, int k, int* result_size, int** result_column_sizes) {
    unsigned long long comb_count = 1;
    for (int i = n; i > n - k; --i) comb_count *= i;
    for (int i = k; i > 1; --i) comb_count /= i;
    if (comb_count > INT_MAX) {
        *result_size = 0;
        *result_column_sizes = NULL;
        return NULL;
    }
    int** result = malloc(comb_count * sizeof(int*));
    *result_size = 0;
    int* temp_comb = malloc(k * sizeof(int));
    *result_column_sizes = malloc(comb_count * sizeof(int));
    for (int i = 0; i < comb_count; i++) (*result_column_sizes)[i] = k;
    recursive_combination(n, k, temp_comb, 0, 1, result, result_size);
    free(temp_comb);
    return result;
}

The provided C program offers a way to generate all possible combinations of K numbers out of a total of N numbers. The structure of the solution involves two main functions: recursive_combination and get_combinations.

Understanding recursive_combination function:

  • This function recursively builds combinations using a depth-first approach.
  • Combines integers from a set range based on the selected count (select_count).
  • Stores each valid combination once the required size (select_count) is reached.
  • Manages current indices and prevents range overflows ensuring that only valid combinations are stored.

Breaking down the get_combinations function:

  • Computes the total number of combinations using a combinatorial formula and checks for overflow.
  • Initializes memory for holding all possible combinations.
  • Uses recursive_combination to populate the array with all valid combinations.
  • Returns the results containing pointers to combinations along with size metadata.

Steps in get_combinations:

  1. Compute the total possible number of combinations using a factorial-based formula and handle potential integer overflow.
  2. Allocate memory for storing pointers to all potential combinations.
  3. Allocate memory for each result column size to hold the size of each combination.
  4. Call the recursive_combination to recursively fill in each possible combination into the results array.
  5. Free temporarily used memory post-combination generation.

Workflow of Recursive Combinations Generation:

  • Initiate recursion with starting value and empty current combination.
  • As each valid combination is formed, allocate space and add to results.
  • Incrementally build combinations by raising the start value, preventing duplicate or reversed combinations.

Memory Management:

  • Allocates dynamic memory for storing combinations and intermediate results.
  • Freely allocates and deallocates memory within the recursive stack to optimize space usage during runtime.

By implementing meticulous memory and loop control, this C program efficiently computes all combinations of K elements from a set of N distinct integers, ensuring that all combinations are visited without repetition or omission.

js
var generate_combinations = function(total, subsetSize) {
    const results = [];
    const exploreCombinations = (currentCombination, startIndex) => {
        if (currentCombination.length === subsetSize) {
            results.push([...currentCombination]);
            return;
        }
        const remaining = subsetSize - currentCombination.length;
        const maxPossible = total - startIndex + 1;
        const maxIndex = maxPossible - remaining;
        for (let index = startIndex; index <= startIndex + maxIndex; index++) {
            currentCombination.push(index);
            exploreCombinations(currentCombination, index + 1);
            currentCombination.pop();
        }
    };
    exploreCombinations([], 1);
    return results;
};

The JavaScript code provided efficiently generates all combinations for a given set. Here is an outline of how the function generate_combinations works:

  • Define generate_combinations taking total and subsetSize as parameters that specify the size of the full set and the size of the combinations, respectively.
  • Initialize an empty array results to store the combinations.
  • Define a recursive helper function exploreCombinations which:
    • Accepts currentCombination that keeps track of elements in the current combination.
    • Accepts startIndex that determines the starting point for the next element in the combination.
    • Uses base condition checks if the length of currentCombination equals subsetSize, then adds a copy of the current combination to results.
    • Calculates the remaining number of elements needed to complete the current combination.
    • Determines the maximum possible starting index maxIndex for the current recursive call based on remaining elements and total size.
    • Iterates through possible numbers from startIndex to the computed upper bound, adds number to the current combination, invokes itself recursively and then removes the number to backtrack and explore other possibilities.
  • The function starts the combination exploration from number 1 by calling exploreCombinations([], 1).
  • Finally, returns the results array populated with all possible combinations.

This implementation utilizes the concept of backtracking to explore all potential combinations by adding numbers to the current combination, recursing, and then removing the last number to try the next possibility, ensuring all combinations are considered. The use of startIndex ensures no numbers are repeated and combinations remain unique.

python
class Solution:
    def generate_combinations(self, total: int, subset_size: int) -> List[List[int]]:
        def recurse_comb(current_comb, start_value):
            if len(current_comb) == subset_size:
                all_combinations.append(current_comb.copy())
                return

            remaining = subset_size - len(current_comb)
            remaining_elem = total - start_value + 1
            max_start = remaining_elem - remaining

            for value in range(start_value, start_value + max_start + 1):
                current_comb.append(value)
                recurse_comb(current_comb, value + 1)
                current_comb.pop()

        all_combinations = []
        recurse_comb([], 1)
        return all_combinations

This Python solution implements a method to generate all possible combinations of a given subset size from a set of numbers ranging from 1 to a specified total. The approach utilizes a classic backtracking algorithm, defined in the nested function recurse_comb, to efficiently explore all combination possibilities.

Here's a breakdown of the solution approach:

  • The generate_combinations function accepts two parameters: total which is the maximum number in the set, and subset_size which is the number of elements in each combination.
  • Inside, an inner function recurse_comb is defined that uses recursion to generate the combinations:
    • It checks if the current combination has reached the desired subset_size and if so, adds it to a list all_combinations.
    • It calculates remaining as how many more elements are needed to complete the current combination.
    • remaining_elem calculates how many numbers are left to choose from, and based on this, the function sets the maximum feasible starting point for iteration to ensure all elements can be included (max_start).
    • A loop iterates through the valid range, adding elements to the current combination, recursing to fill the remaining positions, and then backtracking by removing the last added element (thus exploring other possibilities).
  • An all_combinations list is initialized to store the successfully formed combinations.
  • The recursion is initially triggered with an empty list and starting value 1.
  • Finally, all_combinations containing all the unique combinations is returned.

This solution efficiently navigates through all potential combinations by ensuring that no unnecessary branches are explored in the recursive process, making it optimal for cases where total and subset_size values are large.

Comments

No comments yet.