Problem 599: Distinct Colourings of a Rubik's Cube

View on Project Euler

Project Euler Problem 599 Solution

EulerSolve provides an optimized solution for Project Euler Problem 599, Distinct Colourings of a Rubik's Cube, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary We colour the 24 visible stickers of a \(2\times 2\times 2\) Rubik's Cube using \(n\) available colours. Two colourings are considered equivalent if one can be transformed into the other by legal cube moves. The goal is to count the number \(N(n)\) of distinct orbit classes, with the published target \(n=10\) and the useful checkpoint \(N(2)=183\). Mathematical Approach This is an orbit-counting problem for the cube move group acting on 24 sticker positions. The implementations evaluate the same Burnside average in two equivalent ways: one by direct enumeration of all reachable cube states, and one by compressing the sum according to the cycle type of the corner permutation. Step 1: Describe a Cube State by Corners and Twists A legal \(2\times 2\times 2\) state is determined by a permutation of the 8 corner cubies together with a twist value \(o_i\in\{0,1,2\}\) for each corner. The twists are constrained by $$\sum_{i=1}^{8} o_i \equiv 0 \pmod{3}.$$ So the number of reachable states is $$|G|=8!\cdot 3^7=88{,}179{,}840.$$ This is the group over which Burnside's lemma is averaged. Step 2: Apply Burnside's Lemma to Sticker Colourings Each group element \(g\in G\) induces a permutation of the 24 stickers. Let \(c(g)\) be the number of cycles in that sticker permutation....

Detailed mathematical approach

Problem Summary

We colour the 24 visible stickers of a \(2\times 2\times 2\) Rubik's Cube using \(n\) available colours. Two colourings are considered equivalent if one can be transformed into the other by legal cube moves. The goal is to count the number \(N(n)\) of distinct orbit classes, with the published target \(n=10\) and the useful checkpoint \(N(2)=183\).

Mathematical Approach

This is an orbit-counting problem for the cube move group acting on 24 sticker positions. The implementations evaluate the same Burnside average in two equivalent ways: one by direct enumeration of all reachable cube states, and one by compressing the sum according to the cycle type of the corner permutation.

Step 1: Describe a Cube State by Corners and Twists

A legal \(2\times 2\times 2\) state is determined by a permutation of the 8 corner cubies together with a twist value \(o_i\in\{0,1,2\}\) for each corner. The twists are constrained by

$$\sum_{i=1}^{8} o_i \equiv 0 \pmod{3}.$$

So the number of reachable states is

$$|G|=8!\cdot 3^7=88{,}179{,}840.$$

This is the group over which Burnside's lemma is averaged.

Step 2: Apply Burnside's Lemma to Sticker Colourings

Each group element \(g\in G\) induces a permutation of the 24 stickers. Let \(c(g)\) be the number of cycles in that sticker permutation. A colouring is fixed by \(g\) if and only if every sticker cycle is monochromatic, so the number of fixed colourings is

$$\operatorname{Fix}(g)=n^{c(g)}.$$

Therefore the number of distinct colourings is

$$N(n)=\frac{1}{|G|}\sum_{g\in G} n^{c(g)}.$$

The entire problem is now reduced to understanding \(c(g)\) efficiently.

Step 3: Reduce the Cycle Count to Corner Cycles

Write the corner permutation of \(g\) as disjoint cycles. Consider one such corner cycle of length \(\ell\). If the twists encountered around that cycle are \(t_1,\dots,t_\ell\), define its total twist residue by

$$S\equiv t_1+t_2+\cdots+t_\ell \pmod{3}.$$

After following a sticker once around those \(\ell\) corners, the sticker returns to the starting corner but rotated by \(S\). That gives exactly two possibilities:

$$\phi(S)= \begin{cases} 3,&S=0,\\ 1,&S\in\{1,2\}. \end{cases}$$

If \(S=0\), the 3 local sticker tracks remain separate, giving 3 sticker cycles of length \(\ell\). If \(S=1\) or \(2\), those 3 tracks merge into a single sticker cycle of length \(3\ell\).

Step 4: Count Corner Permutations by Cycle Type

Let the corner permutation have cycle type

$$\lambda=1^{m_1}2^{m_2}\cdots 8^{m_8},\qquad \sum_{r=1}^{8} r\,m_r=8.$$

If

$$k=\sum_{r=1}^{8} m_r$$

is the number of corner cycles, then the number of corner permutations with this type is

$$A(\lambda)=\frac{8!}{\prod_{r=1}^{8} r^{m_r}m_r!}.$$

This is the standard formula for the number of permutations with a prescribed cycle decomposition.

Step 5: Count Twist Assignments for a Fixed Cycle Type

For the \(k\) corner cycles, record only their total twist residues \(S_1,\dots,S_k\in\{0,1,2\}\). The global twist rule becomes

$$\sum_{j=1}^{k} S_j \equiv 0 \pmod{3}.$$

If the \(j\)-th corner cycle has length \(\ell_j\), then once \(S_j\) is fixed there are \(3^{\ell_j-1}\) actual twist assignments on that cycle. Multiplying over all cycles gives

$$\prod_{j=1}^{k} 3^{\ell_j-1}=3^{8-k}.$$

So for a fixed cycle type \(\lambda\) and a fixed residue pattern \(S_1,\dots,S_k\) satisfying the congruence, there are exactly \(A(\lambda)\,3^{8-k}\) group elements with that local behaviour.

Step 6: Assemble the Compressed Burnside Formula

The sticker-cycle count contributed by the residue pattern is

$$c=\sum_{j=1}^{k}\phi(S_j).$$

Hence

$$\boxed{ N(n)=\frac{1}{8!\,3^7} \sum_{\lambda\vdash 8} A(\lambda)\,3^{8-k} \sum_{\substack{S_1,\dots,S_k\in\{0,1,2\}\\ \sum S_j\equiv 0 \pmod{3}}} n^{\sum_{j=1}^{k}\phi(S_j)} }.$$

This is the class-compressed form used by the Python and Java implementations. The C++ implementation evaluates the same quantity by enumerating every one of the \(8!\cdot 3^7\) reachable states directly and computing the corresponding 24-sticker cycle count.

Worked Example: One Corner Cycle of Length 2

Suppose one cycle of the corner permutation has length \(\ell=2\). It moves 2 corners and therefore 6 stickers.

If its total twist residue is \(S=0\), then those 6 stickers split into 3 separate 2-cycles, so this corner cycle contributes \(3\) to \(c(g)\).

If its total twist residue is \(S=1\) or \(2\), then the 3 sticker tracks feed into one another and all 6 stickers lie on a single 6-cycle, so the contribution is \(1\).

This local rule is the heart of the method: once the corner cycle structure and the twist sums modulo 3 are known, the number of fixed colourings is immediately \(n^{c(g)}\).

How the Code Works

The C++, Python, and Java implementations all compute the Burnside numerator

$$\sum_{g\in G} n^{c(g)}$$

in big-integer arithmetic and divide by \(8!\cdot 3^7\) at the end.

The C++ implementation explicitly generates all 8! corner permutations and all \(3^7\) valid twist vectors, constructs the induced permutation on 24 stickers, counts its cycle decomposition, and accumulates a histogram by sticker-cycle count. Once the histogram is known, the orbit count is evaluated as

$$N(n)=\frac{1}{|G|}\sum_{k=0}^{24} f_k n^k,$$

where \(f_k\) is the number of group elements having exactly \(k\) sticker cycles.

The Python and Java implementations avoid explicit 24-sticker permutations. Instead, they enumerate the partitions of 8, compute how many corner permutations have each cycle type, iterate over the admissible twist-sum residues for those cycles, and add the corresponding term \(n^c\) with multiplicity \(A(\lambda)\,3^{8-k}\). Both viewpoints are mathematically identical.

A practical checkpoint is

$$N(2)=183,$$

which confirms that the group size, twist constraint, and sticker-cycle logic are all consistent before evaluating the target case \(n=10\).

Complexity Analysis

The C++ implementation performs a direct traversal of all

$$8!\cdot 3^7=88{,}179{,}840$$

reachable states. Counting cycles on 24 stickers takes \(O(24)\) per state, so the total work is \(O(8!\cdot 3^7\cdot 24)\), with small extra memory for the global histogram and thread-local accumulators.

The Python and Java implementations are much more compressed. They iterate over the 22 partitions of 8 and, for a partition with \(k\) parts, over at most \(3^k\le 3^8\) residue patterns. Since the cube size is fixed, this is effectively constant-time orbit counting apart from big-integer arithmetic. Memory usage is \(O(1)\) beyond a small collection of partitions and counters.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=599
  2. Burnside's lemma: Wikipedia — Burnside's lemma
  3. Group action: Wikipedia — Group action
  4. Rubik's Cube group: Wikipedia — Rubik's Cube group
  5. Permutation cycle notation: Wikipedia — Permutation cycle notation

Problem 599 source code

C++

#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>

#include <boost/multiprecision/cpp_int.hpp>

using boost::multiprecision::cpp_int;
using u64 = std::uint64_t;

static int cycles_in_perm(const std::array<std::uint8_t, 24>& p) {
    bool seen[24] = {false};
    int cycles = 0;
    for (int i = 0; i < 24; ++i) {
        if (seen[i]) continue;
        ++cycles;
        int x = i;
        while (!seen[x]) {
            seen[x] = true;
            x = p[(size_t)x];
        }
    }
    return cycles;
}

static std::vector<std::array<std::uint8_t, 8>> build_orientations() {
    std::vector<std::array<std::uint8_t, 8>> oris;
    oris.reserve(2187);
    for (int mask = 0; mask < 2187; ++mask) {
        std::array<std::uint8_t, 8> ori{};
        int t = mask;
        int sum = 0;
        for (int i = 0; i < 7; ++i) {
            const int o = t % 3;
            t /= 3;
            ori[(size_t)i] = (std::uint8_t)o;
            sum += o;
        }
        ori[7] = (std::uint8_t)((3 - (sum % 3)) % 3);
        oris.push_back(ori);
    }
    return oris;
}

static std::vector<std::array<std::uint8_t, 8>> build_permutations() {
    std::vector<std::array<std::uint8_t, 8>> perms;
    perms.reserve(40320);
    std::array<std::uint8_t, 8> perm{};
    for (int i = 0; i < 8; ++i) perm[(size_t)i] = (std::uint8_t)i;
    do {
        perms.push_back(perm);
    } while (std::next_permutation(perm.begin(), perm.end()));
    return perms;
}

struct HistWorkerCtx {
    const std::vector<std::array<std::uint8_t, 8>>* perms;
    const std::vector<std::array<std::uint8_t, 8>>* oris;
    std::size_t begin_idx;
    std::size_t end_idx;
    std::array<u64, 25> local_freq;
};

static void* hist_worker_main(void* ptr) {
    auto* ctx = static_cast<HistWorkerCtx*>(ptr);
    ctx->local_freq.fill(0);

    for (std::size_t pi = ctx->begin_idx; pi < ctx->end_idx; ++pi) {
        const auto& perm = (*ctx->perms)[pi];
        for (const auto& ori : *ctx->oris) {
            std::array<std::uint8_t, 24> p{};
            for (int pos = 0; pos < 8; ++pos) {
                const int c = perm[(size_t)pos];
                const int o = ori[(size_t)pos];
                for (int j = 0; j < 3; ++j) {
                    const int k = (j - o + 3) % 3;
                    p[(size_t)(3 * c + k)] = (std::uint8_t)(3 * pos + j);
                }
            }
            const int cyc = cycles_in_perm(p);
            ++ctx->local_freq[(size_t)cyc];
        }
    }

    return nullptr;
}

static unsigned choose_thread_count(std::size_t tasks) {
    long cpu_count = sysconf(_SC_NPROCESSORS_ONLN);
    unsigned threads = (cpu_count > 0) ? static_cast<unsigned>(cpu_count) : 1U;
    if (threads > 8U) {
        threads = 8U;
    }
    if (threads > tasks) {
        threads = static_cast<unsigned>(tasks);
    }
    if (threads == 0U) {
        threads = 1U;
    }
    return threads;
}

static std::array<u64, 25> cycle_histogram() {
    const auto oris = build_orientations();
    const auto perms = build_permutations();

    std::array<u64, 25> freq{};
    freq.fill(0);

    const unsigned thread_count = choose_thread_count(perms.size());
    if (thread_count == 1U) {
        HistWorkerCtx ctx{&perms, &oris, 0U, perms.size(), {}};
        hist_worker_main(&ctx);
        return ctx.local_freq;
    }

    std::vector<pthread_t> threads(thread_count);
    std::vector<HistWorkerCtx> ctx(thread_count);
    const std::size_t block = (perms.size() + thread_count - 1U) / thread_count;

    unsigned created = 0U;
    bool failed = false;
    for (unsigned t = 0; t < thread_count; ++t) {
        const std::size_t begin = static_cast<std::size_t>(t) * block;
        const std::size_t end = std::min(begin + block, perms.size());
        if (begin >= end) {
            ctx[t] = HistWorkerCtx{&perms, &oris, 0U, 0U, {}};
            continue;
        }
        ctx[t] = HistWorkerCtx{&perms, &oris, begin, end, {}};
        if (pthread_create(&threads[t], nullptr, hist_worker_main, &ctx[t]) != 0) {
            failed = true;
            break;
        }
        ++created;
    }

    for (unsigned t = 0; t < created; ++t) {
        pthread_join(threads[t], nullptr);
    }

    if (failed) {
        HistWorkerCtx seq{&perms, &oris, 0U, perms.size(), {}};
        hist_worker_main(&seq);
        return seq.local_freq;
    }

    for (unsigned t = 0; t < thread_count; ++t) {
        for (int k = 0; k <= 24; ++k) {
            freq[(size_t)k] += ctx[t].local_freq[(size_t)k];
        }
    }

    return freq;
}

static cpp_int orbits_from_hist(const std::array<u64, 25>& freq, int colours) {
    const u64 G = 88179840ULL;

    cpp_int sum = 0;
    cpp_int pw = 1;
    for (int k = 0; k <= 24; ++k) {
        if (freq[(size_t)k]) sum += cpp_int(freq[(size_t)k]) * pw;
        pw *= colours;
    }
    return sum / G;
}

int main() {
    const auto freq = cycle_histogram();
    if (orbits_from_hist(freq, 2) != 183) {
        std::cerr << "Validation failed for n=2\n";
        return 1;
    }

    std::cout << orbits_from_hist(freq, 10) << "\n";
    return 0;
}

Python

import math
from collections import Counter
from itertools import product

def get_parts(n, max_val=8):
    if n == 0: return [[]]
    res = []
    for i in range(min(n, max_val), 0, -1):
        for p in get_parts(n - i, i):
            res.append([i] + p)
    return res

def count_perms(p):
    cnt = math.factorial(8)
    counts = Counter(p)
    for k, v in counts.items():
        cnt //= (k ** v) * math.factorial(v)
    return cnt

def solve_m(m):
    parts = get_parts(8, 8)
    total = 0
    
    for p in parts:
        perms = count_perms(p)
        k = len(p)
        w_inner = 3 ** (8 - k)
        
        for seq in product(range(3), repeat=k):
            if sum(seq) % 3 != 0: continue
            
            c_g = 0
            for S in seq:
                if S == 0:
                    c_g += 3
                else:
                    c_g += 1
            total += perms * w_inner * (m ** c_g)
            
    return total // 88179840

def solve():
    return str(solve_m(10))

if __name__ == '__main__':
    print(solve())

Java

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Euler599 {

    static List<List<Integer>> getParts(int n, int maxVal) {
        List<List<Integer>> res = new ArrayList<>();
        if (n == 0) {
            res.add(new ArrayList<>());
            return res;
        }
        for (int i = Math.min(n, maxVal); i >= 1; i--) {
            for (List<Integer> p : getParts(n - i, i)) {
                List<Integer> lst = new ArrayList<>();
                lst.add(i);
                lst.addAll(p);
                res.add(lst);
            }
        }
        return res;
    }

    static long factorial(int n) {
        long r = 1;
        for (int i = 2; i <= n; i++)
            r *= i;
        return r;
    }

    static long countPerms(List<Integer> p) {
        long cnt = factorial(8);
        Map<Integer, Integer> counts = new HashMap<>();
        for (int x : p)
            counts.put(x, counts.getOrDefault(x, 0) + 1);
        for (Map.Entry<Integer, Integer> e : counts.entrySet()) {
            cnt /= (long) Math.pow(e.getKey(), e.getValue()) * factorial(e.getValue());
        }
        return cnt;
    }

    static BigInteger solveM(int m) {
        List<List<Integer>> parts = getParts(8, 8);
        BigInteger total = BigInteger.ZERO;
        BigInteger bigM = BigInteger.valueOf(m);

        for (List<Integer> p : parts) {
            long perms = countPerms(p);
            int k = p.size();
            long wInner = (long) Math.pow(3, 8 - k);

            int maxMask = (int) Math.pow(3, k);
            for (int mask = 0; mask < maxMask; mask++) {
                int t = mask;
                int sum = 0;
                int cG = 0;
                for (int i = 0; i < k; i++) {
                    int S = t % 3;
                    t /= 3;
                    sum += S;
                    if (S == 0)
                        cG += 3;
                    else
                        cG += 1;
                }
                if (sum % 3 != 0)
                    continue;

                BigInteger term = BigInteger.valueOf(perms).multiply(BigInteger.valueOf(wInner)).multiply(bigM.pow(cG));
                total = total.add(term);
            }
        }
        return total.divide(BigInteger.valueOf(88179840));
    }

    public static String solve() {
        return solveM(10).toString();
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}