每日算法 — 使用java实现群论:置换群与Burnside引理的组合计数

引言

群论(Group Theory)是现代数学的基石之一,也是组合计数中解决”在等价关系下计数”问题的利器。当我们需要计算在旋转、翻转等对称操作下本质不同的方案数时,直接枚举往往会因对称性而导致大量重复计数。Burnside引理和Pólya计数定理正是解决这类问题的经典工具。本文将用Java实现置换群的基本操作与Burnside引理,以项链染色问题为例,展示群论在算法竞赛与工程实践中的应用。

群与置换群的基本概念

群是一个集合 G 配上一个二元运算 ·,满足四个性质:
1. 封闭性:∀a,b∈G, a·b ∈ G
2. 结合律:(a·b)·c = a·(b·c)
3. 单位元:∃e∈G, ∀a∈G, e·a = a·e = a
4. 逆元:∀a∈G, ∃a⁻¹∈G, a·a⁻¹ = e

置换群是由一组置换(即双射函数)构成的群。在组合计数中,置换通常表示为对称操作(如旋转、翻转)。一个 n 元置换可表示为:

σ = ( 1  2  3  ...  n )
    ( σ(1) σ(2) σ(3) ... σ(n) )

Burnside引理

Burnside引理是计算在群作用下不同轨道(等价类)数量的核心定理:

轨道数 = (1/|G|) · Σ_{g∈G} |Fix(g)|

其中:
– G 是作用在集合 X 上的置换群
– |G| 是群的阶(元素个数)
– Fix(g) 是在置换 g 作用下保持不变的元素的集合
– |Fix(g)| 是被 g 固定的元素个数

直观理解:把所有置换的”不动点”数求平均,就得到了不等价的方案数。

Pólya计数定理

Pólya定理是Burnside引理在染色问题上的具体化。设用 m 种颜色对 n 个位置染色,置换 g 可分解为 c(g) 个不相交的轮换。则:

|Fix(g)| = m^{c(g)}

即每个轮换内的位置必须染同色,才能在该置换下保持不变。最终:

不等价染色方案数 = (1/|G|) · Σ_{g∈G} m^{c(g)}

经典问题:项链染色计数

问题描述:用 m 种颜色给 n 颗珠子的项链染色,考虑旋转等价(圆排列),求不等价的染色方案数。

置换群分析:旋转群 G = {旋转0格, 旋转1格, ..., 旋转(n-1)格},共 n 个元素。

对于旋转 k 格,置换可分解为 gcd(n, k) 个长度为 n/gcd(n,k) 的轮换。

因此 c(旋转k格) = gcd(n, k),代入Pólya定理:

ans = (1/n) · Σ_{k=0}^{n-1} m^{gcd(n,k)}

Java实现

以下实现包含置换表示、轮换分解、Burnside引理与Pólya定理的完整求解:

import java.util.*;

/**
 * 群论基础:置换群、Burnside引理与Pólya计数定理
 * 以项链染色问题为例
 */
public class GroupTheory {

    // ============ 置换类 ============

    static class Permutation {
        int[] p; // p[i] 表示位置i映射到p[i]
        int n;

        Permutation(int[] p) {
            this.p = p.clone();
            this.n = p.length;
        }

        // 单位置换
        static Permutation identity(int n) {
            int[] id = new int[n];
            for (int i = 0; i < n; i++) id[i] = i;
            return new Permutation(id);
        }

        // 旋转移置:循环左移k位
        static Permutation rotation(int n, int k) {
            int[] r = new int[n];
            for (int i = 0; i < n; i++) {
                r[i] = (i + k) % n;
            }
            return new Permutation(r);
        }

        // 翻转移置:以轴axis翻转 (axis=0为垂直轴)
        static Permutation reflection(int n, int axis) {
            int[] ref = new int[n];
            for (int i = 0; i < n; i++) {
                ref[i] = (2 * axis - i + n) % n;
            }
            return new Permutation(ref);
        }

        // 复合置换:先应用other,再应用this
        Permutation compose(Permutation other) {
            int[] result = new int[n];
            for (int i = 0; i < n; i++) {
                result[i] = this.p[other.p[i]];
            }
            return new Permutation(result);
        }

        // 分解为不相交轮换,返回轮换个数
        int countCycles() {
            boolean[] visited = new boolean[n];
            int cycles = 0;
            for (int i = 0; i < n; i++) {
                if (!visited[i]) {
                    cycles++;
                    int cur = i;
                    while (!visited[cur]) {
                        visited[cur] = true;
                        cur = p[cur];
                    }
                }
            }
            return cycles;
        }

        // 打印轮换分解
        List<List<Integer>> getCycles() {
            boolean[] visited = new boolean[n];
            List<List<Integer>> cycles = new ArrayList<>();
            for (int i = 0; i < n; i++) {
                if (!visited[i]) {
                    List<Integer> cycle = new ArrayList<>();
                    int cur = i;
                    while (!visited[cur]) {
                        visited[cur] = true;
                        cycle.add(cur);
                        cur = p[cur];
                    }
                    cycles.add(cycle);
                }
            }
            return cycles;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("(");
            for (int i = 0; i < n; i++) {
                sb.append(i).append("->").append(p[i]);
                if (i < n - 1) sb.append(", ");
            }
            return sb.append(")").toString();
        }
    }

    // ============ 快速幂 ============

    static long modPow(long base, long exp, long mod) {
        long result = 1 % mod;
        base %= mod;
        while (exp > 0) {
            if ((exp & 1) == 1) result = (result * base) % mod;
            base = (base * base) % mod;
            exp >>= 1;
        }
        return result;
    }

    // ============ GCD ============

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

    // ============ Burnside引理求解器 ============

    static class BurnsideSolver {
        /**
         * Pólya计数定理:项链染色(仅考虑旋转)
         * n: 珠子数, m: 颜色数
         */
        static long necklaceRotationOnly(int n, int m, long mod) {
            long sum = 0;
            for (int k = 0; k < n; k++) {
                int cycles = gcd(n, k); // 旋转k格的轮换数 = gcd(n,k)
                sum = (sum + modPow(m, cycles, mod)) % mod;
            }
            // 除以n:使用n的模逆元(若mod为质数且n与mod互质)
            // 这里直接做整数除法,因为Burnside引理保证结果是整数
            return sum / n;
        }

        /**
         * 完整二面体群(旋转+翻转)的项链染色
         * 二面体群D_n有2n个元素:n个旋转 + n个翻转
         */
        static long necklaceDihedral(int n, int m, long mod) {
            long sum = 0;

            // n个旋转
            for (int k = 0; k < n; k++) {
                Permutation rot = Permutation.rotation(n, k);
                sum = (sum + modPow(m, rot.countCycles(), mod)) % mod;
            }

            // n个翻转
            for (int axis = 0; axis < n; axis++) {
                Permutation ref = Permutation.reflection(n, axis);
                sum = (sum + modPow(m, ref.countCycles(), mod)) % mod;
            }

            return sum / (2L * n);
        }

        /**
         * 通用Burnside引理:给定置换群,计算不等价染色数
         */
        static long genericBurnside(List<Permutation> group, int m, long mod) {
            long sum = 0;
            for (Permutation g : group) {
                sum = (sum + modPow(m, g.countCycles(), mod)) % mod;
            }
            // 结果必为整数,但模意义下需用逆元
            return sum / group.size(); // 实际应用中若不能整除需用模逆元
        }
    }

    // ============ 生成二面体群 D_n ============

    static List<Permutation> generateDihedralGroup(int n) {
        List<Permutation> group = new ArrayList<>();
        // 旋转
        for (int k = 0; k < n; k++) {
            group.add(Permutation.rotation(n, k));
        }
        // 翻转
        for (int axis = 0; axis < n; axis++) {
            group.add(Permutation.reflection(n, axis));
        }
        return group;
    }

    // ============ 验证与演示 ============

    public static void main(String[] args) {
        System.out.println("=== 群论:置换群与Burnside引理演示 ===\n");

        // 示例1:置换的轮换分解
        System.out.println("--- 置换与轮换分解 ---");
        Permutation rot3 = Permutation.rotation(6, 2);
        System.out.println("旋转2格 (n=6): " + rot3);
        System.out.println("轮换分解: " + rot3.getCycles());
        System.out.println("轮换数: " + rot3.countCycles());

        Permutation ref0 = Permutation.reflection(6, 0);
        System.out.println("\n垂直翻转 (n=6): " + ref0);
        System.out.println("轮换分解: " + ref0.getCycles());
        System.out.println("轮换数: " + ref0.countCycles());

        // 示例2:项链染色(仅旋转)
        System.out.println("\n--- 项链染色计数(仅旋转等价) ---");
        int[] testN = {3, 4, 5, 6};
        int[] testM = {2, 3, 2, 3};
        long MOD = 1_000_000_007L;
        for (int i = 0; i < testN.length; i++) {
            int n = testN[i], m = testM[i];
            long ans = BurnsideSolver.necklaceRotationOnly(n, m, MOD);
            System.out.printf("n=%d, m=%d -> 不等价方案数 = %d%n", n, m, ans);
        }

        // 示例3:项链染色(旋转+翻转)
        System.out.println("\n--- 项链染色计数(旋转+翻转等价) ---");
        for (int i = 0; i < testN.length; i++) {
            int n = testN[i], m = testM[i];
            long ans = BurnsideSolver.necklaceDihedral(n, m, MOD);
            System.out.printf("n=%d, m=%d -> 不等价方案数 = %d%n", n, m, ans);
        }

        // 示例4:生成完整二面体群并验证
        System.out.println("\n--- 二面体群 D_4 ---");
        List<Permutation> d4 = generateDihedralGroup(4);
        System.out.println("群阶 |D_4| = " + d4.size());
        for (Permutation g : d4) {
            System.out.printf("  %s -> 轮换数=%d%n", g, g.countCycles());
        }

        // 示例5:验证Burnside引理(3色染4珠项链)
        System.out.println("\n--- 验证:3色染4珠项链 ---");
        long ansRot = BurnsideSolver.necklaceRotationOnly(4, 3, MOD);
        long ansDih = BurnsideSolver.necklaceDihedral(4, 3, MOD);
        System.out.printf("仅旋转等价: %d%n", ansRot);
        System.out.printf("旋转+翻转等价: %d%n", ansDih);
    }
}

关键设计要点解析

置换的复合运算:置换 σ 和 τ 的复合 στ 表示”先执行 τ,再执行 σ“。代码中通过 result[i] = this.p[other.p[i]] 实现。注意置换群的乘法一般不满足交换律。

轮换分解算法:从每个未访问的位置出发,沿着置换的映射关系追踪,直到回到起点,形成一个轮换。该算法时间复杂度为 O(n),因为每个位置恰好被访问一次。

Burnside引理的整数性:定理保证最终结果 (1/|G|) · Σ|Fix(g)| 必为整数。但在模意义下计算时,需要计算 |G| 的模逆元。当模数为质数且与 |G| 互质时,可用费马小定理求逆元。

复杂度分析

  • 轮换分解:O(n),每个位置访问一次
  • Pólya定理(项链旋转):O(n log m),需计算 n 次快速幂
  • Pólya定理(二面体群):O(n · (n + log m)),共 2n 个置换,每个置换分解 O(n)
  • 空间复杂度:O(n),存储置换数组

扩展方向

  1. 着色多项式:图着色问题中,Pólya定理可推广为计算图的自同构群作用下的不等价着色数。
  2. Burnside引理在数据结构中的应用:在计算循环同构字符串的不同个数时,Burnside引理提供 O(n) 解法,优于 O(n²) 的朴素比较。
  3. 抽象代数视角:当置换群进一步推广到矩阵群、线性变换群时,Burnside引理可应用于编码理论中不等价码字的计数。