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.
| Objects | Group | Order | Count with colours |
|---|---|---|---|
| necklaces of beads | rotations | (rotation by has cycles) | |
| bracelets | dihedral | necklace sum plus ( odd) or ( even), over | |
| necklaces with exactly beads of colour | ; two colours: | ||
| aperiodic necklaces = Lyndon words | (Lyndon Words) | ||
| grid | rotations | 4 | |
| grid | 8 | add (axis and diagonal mirrors), over 8 | |
| cube faces | rotations | 24 | |
| cube vertices | rotations | 24 | |
| cube edges | rotations | 24 | |
| graphs on vertices up to isomorphism | on the edges | sum 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];
}Proof: Burnside, Pólya and the formulas
Burnside. Count pairs with in two ways: . By the orbit–stabiliser theorem , so each orbit contributes , and the total is times the number of orbits. Colourings: fixes a colouring iff the colour is invariant along each cycle of on the positions. Pólya: apply Burnside to the colourings with the prescribed multiplicities; a fixed colouring assigns one colour per cycle, a cycle of length contributes beads of its colour, so the fixed colourings of are counted by the coefficient of in .
Necklaces. Rotation by has cycles of length ; the number of with is . With prescribed counts, a rotation whose cycles have length fixes a colouring iff it is periodic with period , which needs for all , and then there are such colourings; there are rotations with cycle length , giving the formula after renaming . Lyndon words: a word of length has a primitive root of length , so and Möbius inversion gives . Bracelets: for odd every reflection fixes one bead and pairs the rest ( cycles); for even , reflections fix two opposite beads ( cycles) and fix none ( cycles).
Grids and cubes. Quarter turns of an grid have cycles (4-cycles plus the centre for odd ), the half turn , an axis mirror , a diagonal mirror . The cube’s 24 rotations: identity, 6 face quarter turns, 3 face half turns, 8 vertex turns, 6 edge half turns; counting their cycles on faces, vertices and edges gives the three formulas. All were checked by
burnsideon groups generated from two rotations.Graphs. acts on the potential edges and a graph is a 2-colouring of them. For a vertex permutation with cycle lengths : an edge inside a cycle of length lies in an edge cycle of length , except the “diameters” of an even cycle, which have length , giving edge cycles inside each vertex cycle; edges between cycles of lengths form cycles of length . The number of permutations of cycle type with cycles of length is . Checked against the known values through and against brute force (canonical forms) for .
Permutation groups
| Operation | Method | Cost |
|---|---|---|
| order of , membership | Schreier–Sims stabiliser chain | -ish (simple incremental version), fine for |
| with | merge equal-length cycles in groups of with , interleave with step | |
| bijection with , not lexicographic | Myrvold–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; }Proof: Schreier–Sims, permutation roots and the encodings
Schreier–Sims. Let be the subgroup fixing every point (so , trivial). Level stores, for each in the orbit of under , a representative
sigma[k][t]mapping .checksifts: if then must be in the orbit, and fixes as well, i.e. lies in .insadds a non-member as a new generator of level and closes the orbit:updateXextends the orbit with every product of a level- generator and a known representative, and products that land on a known point produce Schreier generators of , which are inserted recursively. By Schreier’s lemma these generate the stabiliser, so at the end every level stores a complete transversal of in and , whichorderreturns; membership is the sift. Checked against BFS closure of random generators for .Roots. A cycle of length of splits in into cycles of length (positions modulo ). So ‘s cycles of length must come from -cycles of length with , each producing exactly of them. The condition is and , so every valid is a multiple of , which is valid itself: a root exists iff divides the number of length- cycles for every , and the smallest valid divisor of the count found by the loop is . Interleaving: placing cycle ‘s -th element at position is injective ( forces , then forces ), and moves position to , i.e. along cycle of .
Encodings.
encodePermutationwrites as the sequence of indices chosen from a shrinking array (swap-with-last removal); this is a mixed-radix number with radices , anddecodePermutationreplays the same removals, so the maps are inverse bijections. Factoradic: every has a unique representation with (mixed radix). All verified exhaustively for .