每日算法 — 使用java实现容斥原理:集合计数与莫比乌斯反演的组合数学应用

引言

容斥原理(Inclusion-Exclusion Principle)是组合数学中最基本也最强大的计数工具之一。当我们需要计算满足某些条件的元素个数,而这些条件之间存在重叠时,直接计数往往会导致重复或遗漏。容斥原理通过系统地加减交集的方式,精确地消除了重复计数。本文将用Java实现容斥原理的通用求解框架,并延伸至莫比乌斯反演,帮助读者掌握这类经典组合计数问题的算法实现。

容斥原理的数学基础

给定有限集合 U 和 n 个性质 P₁, P₂, ..., Pₙ,设 Aᵢ 为具有性质 Pᵢ 的元素集合。容斥原理公式为:

|A₁ ∪ A₂ ∪ ... ∪ Aₙ| = Σ|Aᵢ| - Σ|Aᵢ ∩ Aⱼ| + Σ|Aᵢ ∩ Aⱼ ∩ Aₖ| - ... + (-1)^(n+1)|A₁ ∩ ... ∩ Aₙ|

等价地,计算”不具有任何性质”的元素个数:

|U - (A₁ ∪ ... ∪ Aₙ)| = |U| - Σ|Aᵢ| + Σ|Aᵢ ∩ Aⱼ| - ... + (-1)ⁿ|A₁ ∩ ... ∩ Aₙ|

该公式的核心思想是:先粗略减去所有单个集合的大小,再加回被多减的交集,再减去被多加的三重交集,依此类推。

经典问题:与K互质的数的个数

给定正整数 N 和 K,求区间 [1, N] 中与 K 互质(最大公约数为1)的整数个数。

算法思路:
1. 对 K 进行质因数分解,得到所有不同质因子 p₁, p₂, ..., pₘ
2. 使用容斥原理计算能被至少一个质因子整除的数的个数
3. 用总数 N 减去上述结果,即得与 K 互质的数的个数

例如 N = 30, K = 12 = 2² × 3,质因子为 {2, 3}:
– 能被2整除的数:⌊30/2⌋ = 15
– 能被3整除的数:⌊30/3⌋ = 10
– 能被6整除的数:⌊30/6⌋ = 5
– 结果 = 30 – (15 + 10) + 5 = 10

莫比乌斯函数与莫比乌斯反演

莫比乌斯函数 μ(n) 是数论中的重要函数:

μ(n) =  1   若n是无平方因子数且有偶数个质因子
       -1   若n是无平方因子数且有奇数个质因子
        0   若n含有平方因子

莫比乌斯反演定理:若 F(n) = Σ_{d|n} f(d),则 f(n) = Σ_{d|n} μ(d) · F(n/d)。

在容斥问题中,莫比乌斯反演提供了更高效的计算方式。对于”与K互质”问题,答案可直接表示为:

ans = Σ_{d|K} μ(d) · ⌊N/d⌋

Java实现

以下实现包含容斥原理的通用框架、质因数分解、以及基于莫比乌斯反演的高效解法:

import java.util.*;

/**
 * 容斥原理与莫比乌斯反演的Java实现
 * 应用场景:集合计数、互质数统计、子集枚举
 */
public class InclusionExclusion {

    // ============ 基础工具方法 ============

    // 对n进行质因数分解,返回所有不同的质因子
    static List<Long> primeFactors(long n) {
        List<Long> factors = new ArrayList<>();
        for (long i = 2; i * i <= n; i++) {
            if (n % i == 0) {
                factors.add(i);
                while (n % i == 0) n /= i;
            }
        }
        if (n > 1) factors.add(n);
        return factors;
    }

    // 计算GCD
    static long gcd(long a, long b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    // ============ 容斥原理:与K互质的数的个数 ============

    /**
     * 方法1:直接使用容斥原理
     * 枚举所有质因子的非空子集,计算能被子集中所有质因子整除的数的个数
     */
    static long countCoprimeByIE(long n, long k) {
        List<Long> primes = primeFactors(k);
        int m = primes.size();
        long result = 0;

        // 枚举所有非空子集 (用位掩码表示)
        for (int mask = 1; mask < (1 << m); mask++) {
            long lcm = 1; // 子集中质因子的乘积(因都是质数,乘积即LCM)
            int bits = 0; // 子集中元素个数
            for (int i = 0; i < m; i++) {
                if ((mask & (1 << i)) != 0) {
                    lcm *= primes.get(i);
                    bits++;
                }
            }
            // 容斥符号:奇数个元素为减,偶数个元素为加
            long count = n / lcm;
            if (bits % 2 == 1) result += count;
            else result -= count;
        }
        return n - result; // 总数减去不互质的数
    }

    // ============ 莫比乌斯函数与反演 ============

    // 线性筛法预处理莫比乌斯函数(适用于n不超过MAX_N的情况)
    static class MobiusSieve {
        int[] mu;
        boolean[] isPrime;
        List<Integer> primes = new ArrayList<>();

        MobiusSieve(int maxN) {
            mu = new int[maxN + 1];
            isPrime = new boolean[maxN + 1];
            Arrays.fill(isPrime, true);
            mu[1] = 1;
            isPrime[0] = isPrime[1] = false;

            for (int i = 2; i <= maxN; i++) {
                if (isPrime[i]) {
                    primes.add(i);
                    mu[i] = -1; // 质数有一个质因子
                }
                for (int p : primes) {
                    if (i * p > maxN) break;
                    isPrime[i * p] = false;
                    if (i % p == 0) {
                        mu[i * p] = 0; // 含平方因子
                        break;
                    } else {
                        mu[i * p] = -mu[i]; // 质因子个数奇偶性翻转
                    }
                }
            }
        }
    }

    /**
     * 方法2:使用莫比乌斯反演(需要预筛)
     * ans = Σ_{d|K} μ(d) * floor(N/d)
     */
    static long countCoprimeByMobius(long n, long k, MobiusSieve sieve) {
        long result = 0;
        for (long d = 1; d * d <= k; d++) {
            if (k % d == 0) {
                // d是k的因数
                if (d <= sieve.mu.length - 1) {
                    result += sieve.mu[(int)d] * (n / d);
                }
                long d2 = k / d;
                if (d2 != d && d2 <= sieve.mu.length - 1) {
                    result += sieve.mu[(int)d2] * (n / d2);
                }
            }
        }
        return result;
    }

    // ============ 通用容斥框架 ============

    /**
     * 通用容斥计数器
     * 给定n个集合,每个集合用一个 Predicate<T> 表示
     * 计算满足"恰好满足k个条件"或"至少满足1个条件"的元素个数
     */
    static class InclusionExclusionCounter<T> {
        // 计算满足至少一个条件的元素数
        int countUnion(List<T> universe, List<java.util.function.Predicate<T>> conditions) {
            int n = conditions.size();
            int count = 0;
            for (int mask = 1; mask < (1 << n); mask++) {
                List<java.util.function.Predicate<T>> subset = new ArrayList<>();
                for (int i = 0; i < n; i++) {
                    if ((mask & (1 << i)) != 0) subset.add(conditions.get(i));
                }
                int satisfy = 0;
                for (T elem : universe) {
                    if (subset.stream().allMatch(p -> p.test(elem))) satisfy++;
                }
                if (subset.size() % 2 == 1) count += satisfy;
                else count -= satisfy;
            }
            return count;
        }

        // 计算不满足任何条件的元素数
        int countNone(List<T> universe, List<java.util.function.Predicate<T>> conditions) {
            return universe.size() - countUnion(universe, conditions);
        }
    }

    // ============ 测试与演示 ============

    public static void main(String[] args) {
        System.out.println("=== 容斥原理与莫比乌斯反演演示 ===\n");

        // 示例1:计算[1, 100]中与60互质的数的个数
        long N = 100, K = 60;
        System.out.printf("问题:[%d, %d] 中与 %d 互质的数的个数%n", 1L, N, K);
        System.out.printf("60的质因子分解: %s%n", primeFactors(K));

        long ans1 = countCoprimeByIE(N, K);
        System.out.printf("容斥原理结果: %d%n", ans1);

        // 验证:暴力枚举
        long brute = 0;
        for (long i = 1; i <= N; i++) if (gcd(i, K) == 1) brute++;
        System.out.printf("暴力验证结果: %d%n", brute);

        // 示例2:莫比乌斯反演(预筛到1000)
        MobiusSieve sieve = new MobiusSieve(1000);
        long ans2 = countCoprimeByMobius(N, K, sieve);
        System.out.printf("莫比乌斯反演结果: %d%n\n", ans2);

        // 示例3:通用容斥框架
        System.out.println("=== 通用容斥框架演示 ===");
        List<Integer> universe = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        List<java.util.function.Predicate<Integer>> conds = Arrays.asList(
            x -> x % 2 == 0,  // 偶数
            x -> x % 3 == 0,  // 3的倍数
            x -> x > 5        // 大于5
        );
        InclusionExclusionCounter<Integer> counter = new InclusionExclusionCounter<>();
        int unionCount = counter.countUnion(universe, conds);
        int noneCount = counter.countNone(universe, conds);
        System.out.printf(" Universe: %s%n", universe);
        System.out.printf(" 条件:偶数 或 3的倍数 或 大于5%n");
        System.out.printf(" 满足至少一个条件的元素数: %d%n", unionCount);
        System.out.printf(" 不满足任何条件的元素数: %d%n", noneCount);

        // 示例4:莫比乌斯函数表
        System.out.println("\n=== 莫比乌斯函数表 (1-20) ===");
        for (int i = 1; i <= 20; i++) {
            System.out.printf(" μ(%d) = %d%n", i, sieve.mu[i]);
        }
    }
}

关键算法要点解析

位掩码枚举子集:容斥原理需要枚举所有非空子集。当质因子个数 m ≤ 20 时,可使用位掩码 mask(范围 1 到 2^m - 1)高效枚举。mask 的第 i 位为1表示选中第 i 个质因子。

线性筛求莫比乌斯函数:传统方法需要对每个数单独分解质因数,复杂度 O(n√n)。线性筛利用已筛出的质数和莫比乌斯函数的积性,在 O(n) 时间内预处理 1 到 n 的所有莫比乌斯函数值。

积性函数的应用:莫比乌斯函数是积性函数,即当 gcd(a,b)=1 时,μ(ab) = μ(a)μ(b)。这一性质使得线性筛可以递推计算所有值。

复杂度分析

  • 容斥原理(质因子分解+子集枚举):设 K 有 m 个不同质因子,时间复杂度 O(√K + 2^m)。当 K ≤ 10^12 时,m 通常不超过12,完全可行。
  • 莫比乌斯反演(预筛版):预处理 O(MAX_N),单次查询 O(√K)(枚举因数)。适用于多组查询场景。
  • 通用容斥框架:O(2^n · |U| · n),n 为条件数。仅适用于小规模问题。

扩展应用

  1. 欧拉函数计算:φ(n) 可用莫比乌斯反演表示为 φ(n) = n · Σ_{d|n} μ(d)/d,这是数论中极为优美的公式。
  2. 容斥原理在概率中的应用:当事件不独立时,用容斥计算并事件概率:
    P(A∪B∪C) = P(A)+P(B)+P(C) - P(A∩B) - P(A∩C) - P(B∩C) + P(A∩B∩C)
  3. 多重约束计数:如”计算1到N中不被2、3、5中任何一个整除的数的个数”,直接应用容斥即可。