← All Posts
Combinatorics· Part 3 of 16

Group Theory Basics

Why Group Theory in CP?

When you first see “group theory” on a competitive-programming roadmap, the reaction is usually skepticism. Contests care about speed and correctness — why bother with abstract algebra?

The short answer: symmetry counting. Every time a problem says “count the distinct necklaces,” “how many essentially different colourings,” or “number of unique states of a Rubik’s cube after k moves,” you are implicitly working inside a group. Burnside’s lemma and Pólya enumeration — the workhorses covered in Parts 1 and 2 of this module — are theorems about group actions. Understanding the underlying group makes those tools far less mysterious.

Beyond counting, groups appear whenever a problem involves:

Key insight. If a contest problem says “up to symmetry” or “consider rotations/reflections equivalent,” you almost certainly need a group action and Burnside / Pólya.

This post gives you just enough group theory to wield those tools with confidence and to recognise group structure in problems that don’t explicitly mention it.

Groups: Definition and Examples

A group is a set G together with a binary operation • (often written multiplicatively) satisfying four axioms:

Axiom Statement
Closure For all a, b ∈ G, the product a • b ∈ G.
Associativity (a • b) • c = a • (b • c) for all a, b, c.
Identity There exists e ∈ G such that e • a = a • e = a for all a.
Inverse For every a ∈ G, there exists a−1 with a • a−1 = e.

If the operation is also commutative (a • b = b • a), the group is called abelian.

Example 1 — ℤn (integers mod n)

The set {0, 1, …, n − 1} under addition modulo n. The identity is 0, and the inverse of a is n − a. This is an abelian group of order n.

Example 2 — Sn (symmetric group)

The set of all permutations of {1, 2, …, n} under composition. |Sn| = n!. For n ≥ 3 this group is non-abelian: swapping elements 1↔2 then 2↔3 gives a different result than 2↔3 then 1↔2.

Example 3 — Cyclic group Cn

Generated by a single element r with rn = e. Think of the n rotations of a regular n-gon. Every cyclic group of order n is isomorphic to ℤn.

Example 4 — Dihedral group Dn

The symmetries of a regular n-gon: n rotations and n reflections, giving |Dn| = 2n. This is the group you use when a problem says “consider rotations and reflections equivalent.”

Cayley table for ℤ4

+ 0 1 2 3
0 01 23
1 12 30
2 23 01
3 30 12

Every row and column is a permutation of the elements — this is always true in a group (the Latin square property).

Subgroups and Lagrange’s Theorem

A subgroup H ≤ G is a subset of G that is itself a group under the same operation. Equivalently, H is a subgroup if and only if:

  1. H is non-empty.
  2. For all a, b ∈ H, the product a • b−1 ∈ H (the one-step subgroup test).

Cosets

Given H ≤ G and an element g ∈ G, the left coset is gH = {g • h : h ∈ H}. The distinct left cosets partition G into equal-sized blocks, each of size |H|.

Lagrange’s Theorem. If H is a subgroup of a finite group G, then |H| divides |G|. In particular, the number of distinct cosets (the index [G : H]) equals |G| / |H|.

Why this matters for CP:

Worked example

In S3 (|S3| = 6), the rotations of a triangle form a subgroup C3 of size 3. Lagrange confirms 3 | 6, and there are 6/3 = 2 cosets — one consisting of the three rotations and one of the three reflections.

Pitfall. The converse of Lagrange is not true in general: if d divides |G|, there need not exist a subgroup of order d. (A4 has order 12 but no subgroup of order 6.) Don’t assume subgroups of every divisor size exist.

Group Actions, Orbits, and Stabilizers

A group action of G on a set X is a function G × X → X, written (g, x) ↦ g · x, satisfying:

  1. e · x = x for all x ∈ X  (identity acts trivially).
  2. (g • h) · x = g · (h · x) for all g, h ∈ G and x ∈ X  (compatibility).

In CP terms: think of X as the set of configurations (e.g., colourings of a necklace) and G as the set of symmetries (rotations, reflections). The action tells you which configurations are “the same.”

Orbit of an element

The orbit of x ∈ X is the set of all elements reachable from x by applying group elements:

Orb(x) = { g · x : g ∈ G }

Two orbits are either identical or disjoint, so orbits partition X. Each orbit corresponds to one “essentially distinct” configuration.

Stabilizer of an element

The stabilizer of x is the set of group elements that fix x:

Stab(x) = { g ∈ G : g · x = x }

Stab(x) is always a subgroup of G.

Orbit-Stabilizer Theorem. For any x ∈ X:
|Orb(x)| × |Stab(x)| = |G|

Equivalently, the number of elements in x’s orbit equals the index of its stabilizer: |Orb(x)| = [G : Stab(x)] = |G| / |Stab(x)|.

Worked example — square colourings

Colour the four corners of a square with two colours. The symmetry group is D4 with |D4| = 8 (four rotations + four reflections).

Consider the colouring x = (R, R, B, B) where opposite corners share a colour. The stabilizer consists of the identity and the 180° rotation — so |Stab(x)| = 2. By Orbit-Stabilizer, the orbit of x has size 8/2 = 4. Indeed, the four rotations of this colouring yield four distinct colourings that are all “the same up to symmetry.”

Connection to Burnside’s lemma

Burnside’s lemma counts the number of orbits:

|X / G| = (1 / |G|) ∑g ∈ G |Fix(g)|

where Fix(g) = { x ∈ X : g · x = x }. This is a direct consequence of the Orbit-Stabilizer theorem combined with double counting.

Pitfall. In Burnside’s formula the sum is over group elements, not over elements of X. A common bug is iterating over configurations instead of symmetries.

Permutation Groups in CP

Most group-theory problems in CP boil down to permutation groups. A permutation of {0, 1, …, n−1} is stored as a vector p where p[i] is the image of i.

Cycle notation

The permutation [1, 2, 0, 4, 3] sends 0→1, 1→2, 2→0, 3→4, 4→3. In cycle notation: (0 1 2)(3 4). Cycles are disjoint, and the order of the permutation is the LCM of the cycle lengths — here lcm(3, 2) = 6.

Composition

If p and q are permutations, the composition p ∘ q means “apply q first, then p.” In code: r[i] = p[q[i]].

Inverse

The inverse permutation satisfies p−1[p[i]] = i. In code: inv[p[i]] = i.

Applying a permutation k times

To compute pk, use binary exponentiation on the permutation — exactly like fast power for integers, but replacing multiplication with composition. This runs in O(n log k).

// Compose two permutations: apply q first, then p
vector<int> compose(const vector<int>& p, const vector<int>& q) {
    int n = p.size();
    vector<int> r(n);
    for (int i = 0; i < n; i++)
        r[i] = p[q[i]];
    return r;
}

// Permutation exponentiation via binary lifting
vector<int> perm_pow(vector<int> p, long long k) {
    int n = p.size();
    vector<int> result(n);
    iota(result.begin(), result.end(), 0);  // identity
    while (k > 0) {
        if (k & 1) result = compose(result, p);
        p = compose(p, p);
        k >>= 1;
    }
    return result;
}
Contest tip. When a problem says “apply operation k times” and the operation is a permutation of positions, use permutation exponentiation. This handles k up to 1018 in O(n log k).

Order of a permutation

The order of a permutation is the smallest positive k such that pk = identity. It equals the LCM of its cycle lengths.

// Cycle decomposition and order of a permutation
long long perm_order(const vector<int>& p) {
    int n = p.size();
    vector<bool> visited(n, false);
    long long order = 1;
    for (int i = 0; i < n; i++) {
        if (visited[i]) continue;
        int cycle_len = 0;
        int j = i;
        while (!visited[j]) {
            visited[j] = true;
            j = p[j];
            cycle_len++;
        }
        order = lcm(order, (long long)cycle_len);
    }
    return order;
}
Pitfall — overflow. The LCM of cycle lengths can be astronomically large. For n = 50 the maximum order is 4,539,722,891,238 (Landau’s function). Use __int128 or compute the LCM modulo a prime if the problem asks for modular answers. Always check whether the answer fits in a 64-bit integer.

C++ Toolkit

Below is a self-contained C++ toolkit for permutation-group operations that covers the most common contest needs. All permutations are 0-indexed vectors.

#include <bits/stdc++.h>
using namespace std;

// ---- Permutation basics ----

vector<int> identity(int n) {
    vector<int> id(n);
    iota(id.begin(), id.end(), 0);
    return id;
}

vector<int> compose(const vector<int>& p, const vector<int>& q) {
    int n = p.size();
    vector<int> r(n);
    for (int i = 0; i < n; i++) r[i] = p[q[i]];
    return r;
}

vector<int> inverse(const vector<int>& p) {
    int n = p.size();
    vector<int> inv(n);
    for (int i = 0; i < n; i++) inv[p[i]] = i;
    return inv;
}

vector<int> perm_pow(vector<int> p, long long k) {
    int n = p.size();
    auto result = identity(n);
    while (k > 0) {
        if (k & 1) result = compose(result, p);
        p = compose(p, p);
        k >>= 1;
    }
    return result;
}

// ---- Cycle decomposition ----

vector<vector<int>> cycles(const vector<int>& p) {
    int n = p.size();
    vector<bool> vis(n, false);
    vector<vector<int>> cyc;
    for (int i = 0; i < n; i++) {
        if (vis[i]) continue;
        vector<int> c;
        int j = i;
        while (!vis[j]) {
            vis[j] = true;
            c.push_back(j);
            j = p[j];
        }
        if (c.size() > 1) cyc.push_back(c);  // skip fixed points
    }
    return cyc;
}

long long perm_order(const vector<int>& p) {
    long long ord = 1;
    for (auto& c : cycles(p))
        ord = lcm(ord, (long long)c.size());
    // handle fixed points (cycle length 1, lcm unchanged)
    return ord;
}

// ---- Group generation from generators (BFS) ----

// Given a set of generators, enumerate the entire group.
// Use only when the group is small (|G| <= ~10^6).
vector<vector<int>> generate_group(int n, const vector<vector<int>>& gens) {
    map<vector<int>, bool> seen;
    queue<vector<int>> bfs;
    auto id = identity(n);
    seen[id] = true;
    bfs.push(id);
    vector<vector<int>> group;
    group.push_back(id);
    while (!bfs.empty()) {
        auto cur = bfs.front(); bfs.pop();
        for (auto& g : gens) {
            auto nxt = compose(cur, g);
            if (!seen.count(nxt)) {
                seen[nxt] = true;
                bfs.push(nxt);
                group.push_back(nxt);
            }
        }
    }
    return group;
}

// ---- Count fixed points (for Burnside) ----

// Given a permutation g acting on colourings of n positions with c colours,
// the number of colourings fixed by g is c^(number of cycles of g).
long long count_fixed(const vector<int>& g, int c, long long mod) {
    int n = g.size();
    vector<bool> vis(n, false);
    int num_cycles = 0;
    for (int i = 0; i < n; i++) {
        if (!vis[i]) {
            num_cycles++;
            int j = i;
            while (!vis[j]) { vis[j] = true; j = g[j]; }
        }
    }
    // compute c^num_cycles mod mod
    long long result = 1;
    long long base = c % mod;
    int exp = num_cycles;
    while (exp > 0) {
        if (exp & 1) result = result * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return result;
}

Usage example — necklace counting

Count distinct necklaces of length n with c colours (rotations only). The group is Cn = {rotation by 0, 1, …, n−1 positions}.

long long count_necklaces(int n, int c, long long mod) {
    // Build the n rotations as permutations
    vector<vector<int>> rotations(n);
    for (int r = 0; r < n; r++) {
        rotations[r].resize(n);
        for (int i = 0; i < n; i++)
            rotations[r][i] = (i + r) % n;
    }

    long long total = 0;
    for (int r = 0; r < n; r++)
        total = (total + count_fixed(rotations[r], c, mod)) % mod;

    // Burnside: answer = total / |G| = total / n
    // Need modular inverse of n
    // (Assuming mod is prime, use Fermat's little theorem)
    auto mod_pow = [&](long long b, long long e, long long m) {
        long long r = 1; b %= m;
        while (e > 0) { if (e & 1) r = r * b % m; b = b * b % m; e >>= 1; }
        return r;
    };
    long long inv_n = mod_pow(n, mod - 2, mod);
    return total % mod * inv_n % mod;
}
Complexity. The toolkit functions are all O(n) per call (composition, inverse, cycle decomposition) or O(n log k) for exponentiation. Group generation is O(|G| × n × |gens|) and should only be used for small groups.

CP Applications

Application 1 — Rubik’s cube reachability

Each face turn of a Rubik’s cube is a permutation of 48 facets (ignoring centres). The set of all reachable states forms a group generated by six face turns. While the full group has ≈4.3 × 1019 elements, contest problems typically work with restricted subgroups (e.g., only two generators) or ask for the order of a specific move sequence.

Strategy: Represent the state as a permutation, compose move sequences, and use cycle decomposition to find the order (minimum number of repetitions to return to the solved state).

Application 2 — grid symmetry counting

Problem: count the distinct ways to place k identical pieces on an m × n grid, where two placements are the same if one can be obtained from the other by rotation/reflection.

Strategy:

  1. Identify the symmetry group G acting on the grid cells. For a square grid: D4 (8 elements). For a rectangular grid: the Klein four-group V4 (4 elements: identity, 180° rotation, horizontal flip, vertical flip).
  2. Each group element induces a permutation of the m × n cells.
  3. Apply Burnside: for each symmetry, count how many placements of k pieces are fixed. A placement is fixed by a symmetry if and only if the set of occupied cells is invariant under that permutation.

Application 3 — recognising group structure

Not every group-theory problem says “group” in the statement. Look for these patterns:

Problem clue

  • “Apply operation k times”
  • “Minimum moves to reach state X”
  • “Count distinct up to symmetry”
  • “Prove two states are reachable”

Group concept

  • Permutation exponentiation
  • BFS on group / Cayley graph
  • Burnside / Pólya
  • Group generation & membership

Classic contest problem: repeated shuffle

Given an array of n elements and a shuffle pattern (a permutation), find the array after k applications of the shuffle where k can be up to 1018.

Solution: Store the shuffle as a permutation p. Compute pk using binary exponentiation. Apply the resulting permutation to the original array. Total time: O(n log k).

// Apply permutation to array: result[i] = arr[perm[i]]
template <typename T>
vector<T> apply_perm(const vector<int>& perm, const vector<T>& arr) {
    int n = perm.size();
    vector<T> result(n);
    for (int i = 0; i < n; i++)
        result[i] = arr[perm[i]];
    return result;
}

void solve() {
    int n; long long k;
    cin >> n >> k;

    vector<int> arr(n), perm(n);
    for (int i = 0; i < n; i++) cin >> arr[i];
    for (int i = 0; i < n; i++) { cin >> perm[i]; perm[i]--; } // 0-indexed

    auto pk = perm_pow(perm, k);
    auto result = apply_perm(pk, arr);

    for (int i = 0; i < n; i++)
        cout << result[i] << " \n"[i == n - 1];
}
Pitfall — direction of permutation. Some problems define the shuffle as “element at position i moves to position p[i],” while others mean “position i receives the element from position p[i].” These are inverses of each other. Read the problem statement carefully and verify on the sample before coding.

Summary

Concept Key formula / fact CP use
Group Closure + Assoc. + Identity + Inverse Foundation for everything below
Lagrange’s thm. |H| divides |G| Sanity checks, coset counting
Orbit-Stabilizer |Orb(x)| × |Stab(x)| = |G| Counting orbits, Burnside’s lemma
Burnside’s lemma #orbits = (1/|G|) ∑ |Fix(g)| Necklaces, grid colouring
Cycle decomposition Order = lcm of cycle lengths Repeated permutation, puzzle period
Perm. exponentiation O(n log k) k applications of a shuffle

What to remember for contests:

  1. “Up to symmetry” ⇒ identify the group ⇒ Burnside / Pólya.
  2. Permutation × k times ⇒ cycle decomposition + binary exponentiation.
  3. Reachability in a puzzle ⇒ BFS on the group generated by the legal moves.
  4. Always double-check whether a permutation means “where i goes” or “where i comes from.”
  5. Overflow from LCM of cycle lengths is a classic trap — plan your integer type.

Next up: Permanent of a Matrix, where we connect permutation sums to a harder counting problem.