How to Calculate Combinations

When you select items from a group where the order doesn't matter, you're calculating combinations. This differs fundamentally from permutations, where order is significant. If you're choosing 3...

Key Insights

  • Combinations differ from permutations because order doesn’t matter—choosing {A, B, C} is the same as choosing {C, A, B}, making the formula C(n,r) = n!/(r!(n-r)!) rather than n!/(n-r)!
  • Naive factorial implementations fail catastrophically with numbers above 20 due to integer overflow; always cancel common factors before multiplying or use logarithms for large values
  • Python’s math.comb() (3.8+) handles edge cases and optimizations automatically, but understanding the underlying math is essential for debugging, custom constraints, and working in languages without built-in support

Introduction to Combinations

When you select items from a group where the order doesn’t matter, you’re calculating combinations. This differs fundamentally from permutations, where order is significant. If you’re choosing 3 people from a group of 10 to form a committee, ABC is the same selection as CAB—both represent the same committee. That’s a combination. If you were assigning them roles (president, secretary, treasurer), the order would matter, making it a permutation.

Real-world combination problems appear everywhere: lottery systems (pick 6 numbers from 49), poker hands (5 cards from 52), quality control sampling (test 10 units from a batch of 1000), and team formation. Understanding how to calculate combinations is essential for probability analysis, statistical sampling, and algorithm design.

The Mathematical Formula

The combination formula is:

C(n, r) = n! / (r! × (n-r)!)

Where:

  • n = total number of items available
  • r = number of items to select
  • ! = factorial (the product of all positive integers up to that number)

For example, C(5, 2) asks “how many ways can we choose 2 items from 5?”

C(5, 2) = 5! / (2! × 3!) = (5 × 4 × 3 × 2 × 1) / ((2 × 1) × (3 × 2 × 1)) = 120 / (2 × 6) = 120 / 12 = 10

The factorial function grows explosively: 10! = 3,628,800 and 20! = 2,432,902,008,176,640,000. This creates immediate implementation challenges.

Here’s a basic factorial function:

def factorial(n):
    if n < 0:
        raise ValueError("Factorial undefined for negative numbers")
    if n == 0 or n == 1:
        return 1
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

Implementing Basic Combination Calculation

A straightforward implementation applies the formula directly. However, you must handle edge cases carefully:

def combinations_basic(n, r):
    # Edge case validation
    if n < 0 or r < 0:
        raise ValueError("n and r must be non-negative")
    if r > n:
        return 0  # Can't choose more items than available
    if r == 0 or r == n:
        return 1  # One way to choose nothing or everything
    
    # Apply the formula
    numerator = factorial(n)
    denominator = factorial(r) * factorial(n - r)
    return numerator // denominator

# Test cases
print(combinations_basic(5, 2))   # 10
print(combinations_basic(52, 5))  # 2,598,960 (poker hands)
print(combinations_basic(10, 0))  # 1
print(combinations_basic(10, 11)) # 0

This works for small values but fails miserably for larger ones. Try combinations_basic(100, 50) and watch your program struggle or crash. The intermediate factorial values exceed what most integer types can store.

Optimizing for Large Numbers

The key insight is that you don’t need to calculate full factorials. Many terms cancel out. For C(100, 2):

C(100, 2) = 100! / (2! × 98!)

The 98! in the denominator cancels with the first 98 terms of 100!, leaving:

C(100, 2) = (100 × 99) / (2 × 1) = 4,950

Here’s an optimized implementation that cancels before multiplying:

def combinations_optimized(n, r):
    if n < 0 or r < 0:
        raise ValueError("n and r must be non-negative")
    if r > n:
        return 0
    if r == 0 or r == n:
        return 1
    
    # Optimization: C(n, r) = C(n, n-r), so choose the smaller
    r = min(r, n - r)
    
    result = 1
    for i in range(r):
        result *= (n - i)
        result //= (i + 1)
    
    return result

# Now this works efficiently
print(combinations_optimized(100, 50))  # 100891344545564193334812497256
print(combinations_optimized(1000, 500))  # Completes almost instantly

This approach multiplies and divides incrementally, keeping intermediate values manageable. The r = min(r, n - r) optimization reduces iterations since C(100, 98) = C(100, 2).

For even larger numbers or when you need floating-point results, use logarithms:

import math

def combinations_log(n, r):
    if r > n or r < 0:
        return 0
    if r == 0 or r == n:
        return 1
    
    r = min(r, n - r)
    
    # Sum of log factorials
    log_result = (math.lgamma(n + 1) - 
                  math.lgamma(r + 1) - 
                  math.lgamma(n - r + 1))
    
    return round(math.exp(log_result))

Using Built-in Libraries

Modern languages provide optimized combination functions. Use them in production code unless you have specific requirements.

Python (3.8+):

import math

result = math.comb(52, 5)  # 2,598,960
print(result)

# For older Python or more features
from scipy.special import comb
result = comb(52, 5, exact=True)  # exact=True returns integer

JavaScript:

JavaScript lacks a built-in combination function, but you can use libraries:

// Using mathjs library
const math = require('mathjs');
const result = math.combinations(52, 5);  // 2598960

// Or implement your own (optimized version)
function combinations(n, r) {
    if (r > n || r < 0) return 0;
    if (r === 0 || r === n) return 1;
    
    r = Math.min(r, n - r);
    let result = 1;
    
    for (let i = 0; i < r; i++) {
        result *= (n - i);
        result /= (i + 1);
    }
    
    return Math.round(result);
}

Java:

import org.apache.commons.math3.util.CombinatoricsUtils;

long result = CombinatoricsUtils.binomialCoefficient(52, 5);
System.out.println(result);  // 2598960

Use library functions when:

  • You need reliability and don’t want to debug edge cases
  • Performance isn’t critical (though libraries are usually well-optimized)
  • You’re working with standard mathematical operations

Implement your own when:

  • You need to understand the algorithm for educational purposes
  • You have unusual constraints or modifications
  • You’re working in an environment without library access

Practical Applications

Beyond calculating single values, you often need to generate all possible combinations:

from itertools import combinations

def generate_all_combinations(items, r):
    """Generate all r-combinations from items."""
    return list(combinations(items, r))

# Example: All 2-card combinations from a small deck
cards = ['A', 'K', 'Q', 'J', '10']
two_card_hands = generate_all_combinations(cards, 2)
print(f"Total hands: {len(two_card_hands)}")  # 10
print(two_card_hands[:3])  # [('A', 'K'), ('A', 'Q'), ('A', 'J')]

Solving a probability problem:

What’s the probability of getting exactly 2 aces in a 5-card poker hand?

import math

def poker_probability_two_aces():
    # Ways to choose 2 aces from 4
    ways_to_get_2_aces = math.comb(4, 2)
    
    # Ways to choose 3 non-aces from 48
    ways_to_get_3_non_aces = math.comb(48, 3)
    
    # Total favorable outcomes
    favorable = ways_to_get_2_aces * ways_to_get_3_non_aces
    
    # Total possible 5-card hands
    total = math.comb(52, 5)
    
    probability = favorable / total
    return probability

prob = poker_probability_two_aces()
print(f"Probability: {prob:.4f} or {prob*100:.2f}%")
# Probability: 0.0399 or 3.99%

Performance comparison:

import time

def benchmark_combination_methods(n, r):
    # Optimized implementation
    start = time.perf_counter()
    result1 = combinations_optimized(n, r)
    time1 = time.perf_counter() - start
    
    # Library function
    start = time.perf_counter()
    result2 = math.comb(n, r)
    time2 = time.perf_counter() - start
    
    print(f"C({n}, {r}) = {result1}")
    print(f"Custom: {time1*1000:.4f}ms")
    print(f"Library: {time2*1000:.4f}ms")

benchmark_combination_methods(1000, 500)
# Library functions are typically 2-5x faster due to C implementation

Conclusion

Calculating combinations requires understanding both the mathematical formula and implementation challenges. The basic formula C(n,r) = n!/(r!(n-r)!) is straightforward, but naive implementations fail with moderate inputs due to integer overflow.

Always optimize by canceling common factors before multiplication, and leverage the symmetry property C(n,r) = C(n, n-r). For production code, use built-in library functions like Python’s math.comb(), which handle edge cases and optimizations.

Quick Reference Table:

n r C(n,r) Example
52 5 2,598,960 Poker hands
49 6 13,983,816 Lottery combinations
10 3 120 Small team selections
100 2 4,950 Pairwise comparisons

Remember: combinations are about selection without regard to order. When order matters, you need permutations instead. Master both concepts to handle the full range of counting problems in software engineering.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.