Counting objects up to symmetry (necklaces, bracelets, coloured cubes and grids, graphs up to isomorphism), and computing with permutation groups (Schreier–Sims order and membership, -th roots of a permutation, permutation encodings). Modular arithmetic from Modular Arithmetic.

Burnside’s lemma. For a finite group acting on a set ,

the number of orbits is the average number of fixed points. Fixed points are usually easy to count, orbits are not; that asymmetry is the whole value of the lemma.

Recipe for colourings. (1) Identify the group of symmetries acting on the positions (rotations, reflections, coordinate permutations). (2) Decompose each into cycles on the positions. (3) A colouring is fixed by iff it is constant on every cycle, so . (4) Average. When the group is small (a square’s 8 symmetries, a cube’s 24 rotations), generate it from a few generators and count cycles by brute force: less error-prone than deriving formulas.

Pólya enumeration tracks colour multiplicities: with the cycle index ( = number of -cycles), substituting gives a polynomial whose coefficient of counts orbits of colourings using colour exactly times. Burnside is Pólya with all ; Pólya is needed when the group is complicated (reflections, graph automorphisms) and must come from each permutation’s structure rather than a simple gcd.

ObjectsGroupOrderCount with colours
necklaces of beadsrotations (rotation by has cycles)
braceletsdihedral necklace sum plus ( odd) or ( even), over
necklaces with exactly beads of colour ; two colours:
aperiodic necklaces = Lyndon words (Lyndon Words)
gridrotations 4
grid8add (axis and diagonal mirrors), over 8
cube facesrotations24
cube verticesrotations24
cube edgesrotations24
graphs on vertices up to isomorphism on the edgessum over cycle types (partitions of ) below: 1, 1, 2, 4, 11, 34, 156, 1044, 12346, 274668

Practical notes. The division by needs (fine for and 998244353 unless is a multiple of the modulus: for compute exactly or modulo ). For cyclic groups sum over divisors, instead of , which handles up to .

mint necklaces(ll n, mint k) {                             // rotations only, O(sqrt n); n invertible mod p
    vector<ll> pr; ll m = n; for (ll p = 2; p * p <= m; p++) if (m % p == 0) { pr.push_back(p); while (m % p == 0) m /= p; } if (m > 1) pr.push_back(m);
    auto phi = [&](ll x) { ll r = x; for (ll p : pr) if (x % p == 0) r = r / p * (p - 1); return r; };
    mint total = 0;
    for (ll d = 1; d * d <= n; d++) if (n % d == 0) {
        total += mint(phi(n / d)) * k.pow(d);
        if (d != n / d) total += mint(phi(d)) * k.pow(n / d);
    }
    return total / mint(n);
}
 
mint bracelets(ll n, mint k) {
    mint refl = n % 2 ? mint(n) * k.pow((n + 1) / 2) : mint(n / 2) * (k.pow(n / 2) + k.pow(n / 2 + 1));
    return (necklaces(n, k) * mint(n) + refl) / mint(2 * n);
}
 
mint necklacesWithCounts(const vector<int>& m, const Comb& cb) {   // colour j used exactly m[j] times, rotations
    int n = 0, g = 0; for (int x : m) n += x, g = std::gcd(g, x);
    if (n == 0) return 1;
    mint total = 0;
    for (int d = 1; d <= g; d++) if (g % d == 0) {
        int ph = d; for (int x = d, p = 2; x > 1; p++) if (x % p == 0) { ph = ph / p * (p - 1); while (x % p == 0) x /= p; }
        mint multi = cb.f[n / d]; for (int x : m) multi *= cb.fi[x / d];
        total += mint(ph) * multi;
    }
    return total / mint(n);
}
 
vector<vector<int>> generateGroup(const vector<vector<int>>& gens) {   // all elements of <gens>, BFS; small groups only
    int n = sz(gens[0]); vector<int> id(n); iota(all(id), 0);
    set<vector<int>> seen = {id}; vector<vector<int>> q = {id};
    for (int h = 0; h < sz(q); h++) for (auto& g : gens) {
        vector<int> c(n); for (int i = 0; i < n; i++) c[i] = g[q[h][i]];
        if (seen.insert(c).second) q.push_back(c);
    }
    return q;
}
 
mint burnside(const vector<vector<int>>& group, mint k) {  // colourings of positions up to the group
    mint total = 0;
    for (auto& g : group) {
        int n = sz(g), c = 0; vector<char> vis(n, 0);
        for (int i = 0; i < n; i++) if (!vis[i]) { c++; for (int j = i; !vis[j]; j = g[j]) vis[j] = 1; }
        total += k.pow(c);
    }
    return total / mint(sz(group));
}
 
mint graphsUpToIsomorphism(int n, const Comb& cb) {        // Polya over cycle types of S_n acting on edges, n <~ 40
    mint total = 0; vector<int> parts;
    function<void(int, int)> rec = [&](int rem, int mx) {
        if (rem == 0) {
            ll c = 0; mint cnt = cb.f[n]; map<int, int> mult;
            for (int i = 0; i < sz(parts); i++) {
                c += parts[i] / 2; mult[parts[i]]++; cnt *= cb.iv[parts[i]];
                for (int j = i + 1; j < sz(parts); j++) c += std::gcd(parts[i], parts[j]);
            }
            for (auto [len, t] : mult) cnt *= cb.fi[t];            // n! / prod(i^{m_i} m_i!) permutations of this type
            total += cnt * mint(2).pow(c);
            return;
        }
        for (int x = min(rem, mx); x >= 1; x--) { parts.push_back(x); rec(rem - x, x); parts.pop_back(); }
    };
    rec(n, n);
    return total * cb.fi[n];
}

Permutation groups

OperationMethodCost
order of , membershipSchreier–Sims stabiliser chain-ish (simple incremental version), fine for
with merge equal-length cycles in groups of with , interleave with step
bijection with , not lexicographicMyrvold–Ruskey style swap encoding
factoradic digits

Lexicographic rank / unrank: permutationRank / kthPermutation in Counting Fundamentals. Schreier–Sims also solves the Rubik’s Cube group order.

struct PermutationGroup {                                  // Schreier-Sims (incremental, Jerrum-style filter)
    int n; vector<vector<char>> flag; vector<vector<vector<int>>> sigma, gen;   // sigma[k][t]: fixes points > k, maps k to t
    PermutationGroup(int size) : n(size), flag(size, vector<char>(size, 0)), sigma(size, vector<vector<int>>(size)), gen(size) {
        for (int k = 0; k < n; k++) flag[k][k] = 1, sigma[k][k] = identity();
    }
    vector<int> identity() const { vector<int> v(n); iota(all(v), 0); return v; }
    static vector<int> inverse(const vector<int>& v) { vector<int> r(sz(v)); for (int i = 0; i < sz(v); i++) r[v[i]] = i; return r; }
    static vector<int> compose(const vector<int>& a, const vector<int>& b) { vector<int> c(sz(a)); for (int i = 0; i < sz(a); i++) c[i] = a[b[i]]; return c; }   // a after b
    bool check(const vector<int>& cur, int k) const {     // is cur (fixing points > k) in the group?
        if (k <= 0) return true;
        int t = cur[k];
        return flag[k][t] ? check(compose(inverse(sigma[k][t]), cur), k - 1) : false;
    }
    void ins(const vector<int>& cur, int k) {
        if (check(cur, k)) return;
        gen[k].push_back(cur);
        for (int i = 0; i < n; i++) if (flag[k][i]) updateX(compose(cur, sigma[k][i]), k);
    }
    void updateX(const vector<int>& cur, int k) {
        int t = cur[k];
        if (flag[k][t]) ins(compose(inverse(sigma[k][t]), cur), k - 1);
        else { flag[k][t] = 1, sigma[k][t] = cur; for (auto& x : gen[k]) updateX(compose(x, cur), k); }
    }
    void add(const vector<int>& p) { if (n > 0) ins(p, n - 1); }
    __uint128_t order() const {                           // exact for n <= 34 (34! < 2^128)
        __uint128_t tot = 1;
        for (int i = 0; i < n; i++) { int cnt = 0; for (int j = 0; j <= i; j++) cnt += flag[i][j]; tot *= cnt; }
        return tot;
    }
    bool contains(const vector<int>& p) const { return n == 0 || check(p, n - 1); }
};
 
vector<int> permutationRoot(const vector<int>& p, ll k) {  // some q with q^k = p, {} if none
    int n = sz(p); vector<char> seen(n, 0); map<int, vector<vector<int>>> byLen;
    for (int i = 0; i < n; i++) if (!seen[i]) { vector<int> cyc; for (int j = i; !seen[j]; j = p[j]) seen[j] = 1, cyc.push_back(j); byLen[sz(cyc)].push_back(cyc); }
    vector<int> q(n);
    for (auto& [L, cycles] : byLen) {
        int cnt = sz(cycles), g = -1;
        for (int c = 1; c <= cnt; c++) if (cnt % c == 0 && std::gcd((ll)L * c, k) == c) { g = c; break; }
        if (g == -1) return {};
        int N = L * g;
        for (int start = 0; start < cnt; start += g) {
            vector<int> s(N);
            for (int t = 0; t < g; t++) for (int j = 0; j < L; j++) s[(t + (ll)j * k) % N] = cycles[start + t][j];
            for (int i = 0; i < N; i++) q[s[i]] = s[(i + 1) % N];
        }
    }
    return q;
}
 
ull encodePermutation(const vector<int>& b) {             // bijection S_n -> [0, n!), n <= 20
    int n = sz(b); ull a = 0, mul = 1; vector<int> pos(n), el(n); iota(all(pos), 0); iota(all(el), 0);
    for (int i = 0; i < n; i++) {
        int z = pos[b[i]]; a += mul * z; mul *= sz(el);
        swap(pos[el[z]], pos[el.back()]); swap(el[z], el.back()); el.pop_back();
    }
    return a;
}
vector<int> decodePermutation(int n, ull a) {
    vector<int> el(n), b; iota(all(el), 0);
    for (int i = 0; i < n; i++) { int z = a % sz(el); b.push_back(el[z]); a /= sz(el); swap(el[z], el.back()); el.pop_back(); }
    return b;
}
vector<int> toFactoradic(ull x, int n) { vector<int> d(n, 0); for (int i = 1; i <= n; i++) d[n - i] = x % i, x /= i; return d; }   // digit i (from the left) in [0, n - i)
ull fromFactoradic(const vector<int>& d) { int n = sz(d); ull x = 0; for (int i = 0; i < n; i++) x = x * (n - i) + d[i]; return x; }