Combinatorics

Master advanced combinatorial techniques for complex counting problems

Overview

Combinatorics is a core mathematical branch in competitive programming, involving counting, permutations, and combinations. This chapter explores advanced techniques like inclusion-exclusion principle, Catalan numbers, and generating functions to solve complex combinatorial problems in world-class competitions.

Core Content

1. Inclusion-Exclusion Principle

The inclusion-exclusion principle is a powerful tool for complex counting problems, calculating the size of unions through alternating addition and subtraction.

Basic Principle

// Inclusion-Exclusion: Calculate size of union of multiple sets
// |A ∪ B ∪ C| = |A| + |B| + |C| - |A ∩ B| - |A ∩ C| - |B ∩ C| + |A ∩ B ∩ C|

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// Count numbers divisible by certain numbers (1 to n)
long long inclusionExclusion(long long n, vector<int>& divisors) {
    int m = divisors.size();
    long long result = 0;
    
    // Enumerate all subsets
    for (int mask = 1; mask < (1 << m); mask++) {
        long long lcm = 1;
        int bits = 0;
        
        // Calculate LCM of current subset
        for (int i = 0; i < m; i++) {
            if (mask & (1 << i)) {
                lcm = lcm / __gcd(lcm, (long long)divisors[i]) * divisors[i];
                bits++;
                if (lcm > n) break; // Optimization: break if LCM too large
            }
        }
        
        // Add or subtract based on subset size
        if (bits % 2 == 1) {
            result += n / lcm;
        } else {
            result -= n / lcm;
        }
    }
    
    return result;
}

int main() {
    long long n = 100;
    vector<int> divisors = {2, 3, 5};
    
    // Count numbers from 1 to 100 divisible by 2, 3, or 5
    cout << inclusionExclusion(n, divisors) << endl;
    
    return 0;
}

Advanced Application: Derangements

// Derangements: Calculate number of complete derangements of n elements
#include <iostream>
#include <vector>
using namespace std;

// Calculate derangements using inclusion-exclusion
long long derangements(int n) {
    if (n == 0) return 1;
    if (n == 1) return 0;
    
    // D(n) = n! * Σ(k=0 to n) (-1)^k / k!
    long long factorial = 1;
    for (int i = 1; i <= n; i++) {
        factorial *= i;
    }
    
    double sum = 0;
    long long fact = 1;
    for (int k = 0; k <= n; k++) {
        if (k > 0) fact *= k;
        sum += (k % 2 == 0 ? 1.0 : -1.0) / fact;
    }
    
    return (long long)(factorial * sum + 0.5);
}

// Calculate derangements using recurrence (more efficient)
long long derangementsDP(int n) {
    if (n == 0) return 1;
    if (n == 1) return 0;
    
    vector<long long> dp(n + 1);
    dp[0] = 1;
    dp[1] = 0;
    
    for (int i = 2; i <= n; i++) {
        // D(n) = (n-1) * (D(n-1) + D(n-2))
        dp[i] = (long long)(i - 1) * (dp[i - 1] + dp[i - 2]);
    }
    
    return dp[n];
}

int main() {
    for (int n = 0; n <= 10; n++) {
        cout << "D(" << n << ") = " << derangementsDP(n) << endl;
    }
    return 0;
}

2. Catalan Numbers

Catalan numbers have wide applications in combinatorics, from parentheses matching to binary tree counting, key for solving recursive structure problems.

// Multiple methods to calculate Catalan numbers
#include <iostream>
#include <vector>
using namespace std;

// Method 1: Recurrence C(n) = Σ(i=0 to n-1) C(i) * C(n-1-i)
long long catalanDP(int n) {
    vector<long long> catalan(n + 1, 0);
    catalan[0] = catalan[1] = 1;
    
    for (int i = 2; i <= n; i++) {
        for (int j = 0; j < i; j++) {
            catalan[i] += catalan[j] * catalan[i - 1 - j];
        }
    }
    
    return catalan[n];
}

// Method 2: Combinatorial formula C(n) = C(2n, n) / (n + 1)
long long catalanCombination(int n) {
    // Calculate combination C(2n, n)
    long long result = 1;
    for (int i = 0; i < n; i++) {
        result = result * (2 * n - i) / (i + 1);
    }
    return result / (n + 1);
}

// Application 1: Number of valid parentheses sequences
int validParentheses(int n) {
    return catalanDP(n);
}

// Application 2: Number of binary search trees
int numBST(int n) {
    return catalanDP(n);
}

// Application 3: Number of mountain arrays
int mountainArrays(int n) {
    if (n % 2 == 0) return 0; // Even length cannot form mountain
    return catalanDP(n / 2);
}

// Application 4: Triangulations of convex polygon
int triangulations(int n) {
    if (n < 3) return 0;
    return catalanDP(n - 2);
}

int main() {
    int n = 10;
    cout << "Catalan numbers:" << endl;
    for (int i = 0; i <= n; i++) {
        cout << "C(" << i << ") = " << catalanDP(i) << endl;
    }
    
    cout << "\nApplications:" << endl;
    cout << "Valid parentheses with 5 pairs: " << validParentheses(5) << endl;
    cout << "BSTs with 5 nodes: " << numBST(5) << endl;
    cout << "Triangulations of hexagon: " << triangulations(6) << endl;
    
    return 0;
}

3. Advanced Combinatorial Techniques

Generating Functions

Generating functions are powerful tools in combinatorics, transforming combinatorial problems into algebraic ones.

// Generating Functions: Integer Partitions
#include <iostream>
#include <vector>
using namespace std;

// Calculate number of partitions of integer n (order does not matter)
int partitions(int n) {
    vector<int> dp(n + 1, 0);
    dp[0] = 1;
    
    // Process each possible part
    for (int part = 1; part <= n; part++) {
        for (int sum = part; sum <= n; sum++) {
            dp[sum] += dp[sum - part];
        }
    }
    
    return dp[n];
}

// Calculate distinct partitions of n (all parts different)
int distinctPartitions(int n) {
    vector<int> dp(n + 1, 0);
    dp[0] = 1;
    
    for (int part = 1; part <= n; part++) {
        for (int sum = n; sum >= part; sum--) {
            dp[sum] += dp[sum - part];
        }
    }
    
    return dp[n];
}

// Pentagonal Number Theorem: Fast partition calculation
vector<int> fastPartitions(int n) {
    vector<int> p(n + 1, 0);
    p[0] = 1;
    
    for (int i = 1; i <= n; i++) {
        for (int k = 1; ; k++) {
            // Pentagonal numbers: k(3k-1)/2
            int pent1 = k * (3 * k - 1) / 2;
            int pent2 = k * (3 * k + 1) / 2;
            
            if (pent1 > i) break;
            
            // Apply recurrence relation
            if (k % 2 == 1) {
                p[i] += p[i - pent1];
                if (pent2 <= i) p[i] += p[i - pent2];
            } else {
                p[i] -= p[i - pent1];
                if (pent2 <= i) p[i] -= p[i - pent2];
            }
        }
    }
    
    return p;
}

int main() {
    int n = 20;
    
    cout << "Integer partitions:" << endl;
    for (int i = 1; i <= 10; i++) {
        cout << "P(" << i << ") = " << partitions(i) << endl;
    }
    
    cout << "\nDistinct partitions:" << endl;
    for (int i = 1; i <= 10; i++) {
        cout << "Q(" << i << ") = " << distinctPartitions(i) << endl;
    }
    
    // Verify pentagonal number theorem
    vector<int> fast = fastPartitions(n);
    cout << "\nFast algorithm verification: P(" << n << ") = " << fast[n] << endl;
    cout << "Standard algorithm: P(" << n << ") = " << partitions(n) << endl;
    
    return 0;
}

Burnside's Lemma

// Burnside's Lemma: Count distinct colorings under group action
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// Calculate GCD
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

// Count distinct necklace colorings (rotations and reflections)
long long necklaceColorings(int n, int k) {
    if (n == 0) return 1;
    
    long long result = 0;
    
    // Rotational symmetry: for each possible rotation angle
    for (int i = 0; i < n; i++) {
        int cycles = gcd(n, i);
        result += (long long)pow(k, cycles);
    }
    
    // Reflection symmetry
    if (n % 2 == 1) {
        // Odd: n reflection axes, each fixes 1 bead
        result += n * (long long)pow(k, (n + 1) / 2);
    } else {
        // Even: n/2 axes through vertices + n/2 axes through edge midpoints
        result += (n / 2) * (long long)pow(k, n / 2 + 1);
        result += (n / 2) * (long long)pow(k, n / 2);
    }
    
    // Apply Burnside's lemma: average number of fixed points
    return result / (2 * n);
}

// Count distinct square colorings (4 vertices, k colors)
long long squareColorings(int k) {
    // 8 symmetry operations: identity, 3 rotations, 4 reflections
    long long result = 0;
    
    // Identity: all colorings are fixed
    result += (long long)pow(k, 4);
    
    // 90° and 270° rotations: all 4 vertices same color
    result += 2 * k;
    
    // 180° rotation: diagonal vertices same color
    result += (long long)k * k;
    
    // 4 reflections: adjacent vertices paired
    result += 4 * (long long)k * k;
    
    return result / 8;
}

int main() {
    cout << "Necklace coloring problems:" << endl;
    for (int n = 3; n <= 8; n++) {
        for (int k = 2; k <= 4; k++) {
            cout << "n=" << n << ", k=" << k << ": " 
                 << necklaceColorings(n, k) << " distinct colorings" << endl;
        }
    }
    
    cout << "\nSquare colorings:" << endl;
    for (int k = 2; k <= 5; k++) {
        cout << k << " colors: " 
             << squareColorings(k) << " distinct colorings" << endl;
    }
    
    return 0;
}

Problem-Solving Tips

🎯 Problem Recognition

Learn to recognize different combinatorial problems: use inclusion-exclusion for counting, consider Catalan numbers for recursive structures, apply Burnside's lemma for symmetry problems.

⚡ Computational Optimization

Watch for overflow in large number calculations, use modular arithmetic and fast exponentiation. Precomputing combinations and factorials significantly improves efficiency.

🔄 Recurrence Relations

Many combinatorial problems have elegant recurrence relations. Finding state transition equations is often more efficient than direct calculation.

🧮 Mathematical Insights

Deeply understand combinatorial identities and generating functions - they are powerful tools for solving complex combinatorial problems.