组合数学

掌握高级组合数学技巧,解决复杂计数问题

概述

组合数学是算法竞赛中的核心数学分支,涉及计数、排列、组合等问题。本章节深入探讨容斥原理、卡塔兰数、生成函数等高级技巧,为解决世界级竞赛中的复杂组合问题奠定基础。

核心内容

1. 容斥原理

容斥原理是解决复杂计数问题的强大工具,通过加减交替计算集合的并集大小。

基本原理

// 容斥原理:计算多个集合的并集大小
// |A ∪ B ∪ C| = |A| + |B| + |C| - |A ∩ B| - |A ∩ C| - |B ∩ C| + |A ∩ B ∩ C|

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

// 计算能被某些数整除的数的个数(1到n)
long long inclusionExclusion(long long n, vector<int>& divisors) {
    int m = divisors.size();
    long long result = 0;
    
    // 枚举所有子集
    for (int mask = 1; mask < (1 << m); mask++) {
        long long lcm = 1;
        int bits = 0;
        
        // 计算当前子集的LCM
        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; // 优化:LCM过大直接跳出
            }
        }
        
        // 根据子集大小决定加减
        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};
    
    // 计算1到100中能被2、3或5整除的数的个数
    cout << inclusionExclusion(n, divisors) << endl;
    
    return 0;
}

高级应用:错位排列

// 错位排列:计算n个元素的完全错位排列数
#include <iostream>
#include <vector>
using namespace std;

// 使用容斥原理计算错位排列
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);
}

// 递推公式计算错位排列(更高效)
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. 卡塔兰数

卡塔兰数在组合数学中有着广泛应用,从括号匹配到二叉树计数,是解决递归结构问题的关键。

// 卡塔兰数的多种计算方法
#include <iostream>
#include <vector>
using namespace std;

// 方法1:递推公式 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];
}

// 方法2:组合数公式 C(n) = C(2n, n) / (n + 1)
long long catalanCombination(int n) {
    // 计算组合数 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);
}

// 应用1:有效括号序列数量
int validParentheses(int n) {
    return catalanDP(n);
}

// 应用2:二叉搜索树数量
int numBST(int n) {
    return catalanDP(n);
}

// 应用3:山脉数组数量
int mountainArrays(int n) {
    if (n % 2 == 0) return 0; // 偶数长度无法形成山脉
    return catalanDP(n / 2);
}

// 应用4:凸多边形三角剖分数
int triangulations(int n) {
    if (n < 3) return 0;
    return catalanDP(n - 2);
}

int main() {
    int n = 10;
    cout << "Catalan数:" << endl;
    for (int i = 0; i <= n; i++) {
        cout << "C(" << i << ") = " << catalanDP(i) << endl;
    }
    
    cout << "\n应用示例:" << endl;
    cout << "5对括号的有效序列数: " << validParentheses(5) << endl;
    cout << "5个节点的BST数量: " << numBST(5) << endl;
    cout << "六边形三角剖分数: " << triangulations(6) << endl;
    
    return 0;
}

3. 高级组合技巧

生成函数

生成函数是组合数学中的强大工具,能够将组合问题转化为代数问题。

// 生成函数应用:整数分拆
#include <iostream>
#include <vector>
using namespace std;

// 计算整数n的分拆数(不同顺序视为同一分拆)
int partitions(int n) {
    vector<int> dp(n + 1, 0);
    dp[0] = 1;
    
    // 对每个可能的部分进行处理
    for (int part = 1; part <= n; part++) {
        for (int sum = part; sum <= n; sum++) {
            dp[sum] += dp[sum - part];
        }
    }
    
    return dp[n];
}

// 计算整数n的不同分拆数(所有部分都不同)
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];
}

// 五边形数定理应用:快速计算分拆数
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++) {
            // 五边形数:k(3k-1)/2
            int pent1 = k * (3 * k - 1) / 2;
            int pent2 = k * (3 * k + 1) / 2;
            
            if (pent1 > i) break;
            
            // 应用递推关系
            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 << "整数分拆:" << endl;
    for (int i = 1; i <= 10; i++) {
        cout << "P(" << i << ") = " << partitions(i) << endl;
    }
    
    cout << "\n不同分拆:" << endl;
    for (int i = 1; i <= 10; i++) {
        cout << "Q(" << i << ") = " << distinctPartitions(i) << endl;
    }
    
    // 验证五边形数定理
    vector<int> fast = fastPartitions(n);
    cout << "\n快速算法验证:P(" << n << ") = " << fast[n] << endl;
    cout << "标准算法:P(" << n << ") = " << partitions(n) << endl;
    
    return 0;
}

Burnside引理

// Burnside引理:计算在群作用下的不同着色数
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// 计算最大公约数
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

// 计算圆形项链的不同着色数(可旋转和翻转)
long long necklaceColorings(int n, int k) {
    if (n == 0) return 1;
    
    long long result = 0;
    
    // 旋转对称:对每个可能的旋转角度
    for (int i = 0; i < n; i++) {
        int cycles = gcd(n, i);
        result += (long long)pow(k, cycles);
    }
    
    // 翻转对称
    if (n % 2 == 1) {
        // 奇数:n条翻转轴,每条轴固定1个珠子
        result += n * (long long)pow(k, (n + 1) / 2);
    } else {
        // 偶数:n/2条穿过顶点的轴 + n/2条穿过边中点的轴
        result += (n / 2) * (long long)pow(k, n / 2 + 1);
        result += (n / 2) * (long long)pow(k, n / 2);
    }
    
    // 应用Burnside引理:平均不动点数
    return result / (2 * n);
}

// 计算正方形的不同着色数(4个顶点,k种颜色)
long long squareColorings(int k) {
    // 8个对称操作:恒等、3个旋转、4个翻转
    long long result = 0;
    
    // 恒等变换:所有着色都不动
    result += (long long)pow(k, 4);
    
    // 90度和270度旋转:4个顶点必须同色
    result += 2 * k;
    
    // 180度旋转:对角顶点同色
    result += (long long)k * k;
    
    // 4个翻转:相邻顶点成对同色
    result += 4 * (long long)k * k;
    
    return result / 8;
}

int main() {
    cout << "项链着色问题:" << endl;
    for (int n = 3; n <= 8; n++) {
        for (int k = 2; k <= 4; k++) {
            cout << "n=" << n << ", k=" << k << ": " 
                 << necklaceColorings(n, k) << " 种不同着色" << endl;
        }
    }
    
    cout << "\n正方形着色:" << endl;
    for (int k = 2; k <= 5; k++) {
        cout << k << " 种颜色: " 
             << squareColorings(k) << " 种不同着色" << endl;
    }
    
    return 0;
}

解题技巧

🎯 问题识别

学会识别不同类型的组合问题:计数问题用容斥原理,递归结构考虑卡塔兰数,对称问题应用Burnside引理。

⚡ 计算优化

大数计算时注意溢出,使用模运算和快速幂。预计算组合数和阶乘可以显著提高效率。

🔄 递推关系

许多组合问题都有优美的递推关系。寻找状态转移方程,往往比直接计算更高效。

🧮 数学洞察

深入理解组合恒等式和生成函数,它们是解决复杂组合问题的强大工具。