Problem 281: Pizza Toppings
View on Project EulerProject Euler Problem 281 Solution
EulerSolve provides an optimized solution for Project Euler Problem 281, Pizza Toppings, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary For integers \(m\ge2\) and \(n\ge1\), we place \(mn\) toppings around a circle, with exactly \(n\) copies of each of the \(m\) colors. Two arrangements are considered the same if one is a rotation of the other. Let \(f(m,n)\) be the number of distinct rotational classes. The Project Euler task is to sum all values satisfying $$f(m,n)\le 10^{15}.$$ The final numeric sum is intentionally omitted here; the goal is to explain the counting formula and the search bounds used by the C++ solution. Mathematical Approach 1) Burnside's lemma is the natural tool The acting group is the cyclic rotation group $$C_{mn}=\{0,1,\dots,mn-1\},$$ where rotation by \(r\) means shifting every position by \(r\) places around the circle. Burnside's lemma says that the number of rotational classes is the average number of arrangements fixed by each rotation: $$f(m,n)=\frac{1}{mn}\sum_{r=0}^{mn-1}\mathrm{Fix}(r).$$ So the whole problem is to understand \(\mathrm{Fix}(r)\) for one rotation \(r\). 2) Cycle structure of one rotation Set $$N=mn,\qquad d=\gcd(N,r).$$ The rotation by \(r\) decomposes the \(N\) positions into $$d$$ disjoint cycles, each of length $$\ell=\frac{N}{d}.$$ This is the standard cycle decomposition of a cyclic shift....
Detailed mathematical approach
Problem Summary
For integers \(m\ge2\) and \(n\ge1\), we place \(mn\) toppings around a circle, with exactly \(n\) copies of each of the \(m\) colors. Two arrangements are considered the same if one is a rotation of the other. Let \(f(m,n)\) be the number of distinct rotational classes. The Project Euler task is to sum all values satisfying
$$f(m,n)\le 10^{15}.$$
The final numeric sum is intentionally omitted here; the goal is to explain the counting formula and the search bounds used by the C++ solution.
Mathematical Approach
1) Burnside's lemma is the natural tool
The acting group is the cyclic rotation group
$$C_{mn}=\{0,1,\dots,mn-1\},$$
where rotation by \(r\) means shifting every position by \(r\) places around the circle.
Burnside's lemma says that the number of rotational classes is the average number of arrangements fixed by each rotation:
$$f(m,n)=\frac{1}{mn}\sum_{r=0}^{mn-1}\mathrm{Fix}(r).$$
So the whole problem is to understand \(\mathrm{Fix}(r)\) for one rotation \(r\).
2) Cycle structure of one rotation
Set
$$N=mn,\qquad d=\gcd(N,r).$$
The rotation by \(r\) decomposes the \(N\) positions into
$$d$$
disjoint cycles, each of length
$$\ell=\frac{N}{d}.$$
This is the standard cycle decomposition of a cyclic shift.
An arrangement is fixed by this rotation if and only if every cycle is monochromatic: once one position in a cycle is chosen, all other positions in that cycle are forced to have the same color.
3) When can a rotation fix any valid arrangement?
Each color appears exactly \(n\) times. If every cycle has length \(\ell\), then every color must occupy a whole number of cycles. Therefore
$$\ell \mid n$$
is necessary.
It is also sufficient. If \(\ell\mid n\), then each color must occupy exactly
$$\frac{n}{\ell}$$
cycles, because each such cycle contributes \(\ell\) positions of that color.
Since there are
$$d=\frac{N}{\ell}=\frac{mn}{\ell}$$
cycles in total, a fixed arrangement exists precisely when \(\ell\mid n\).
4) Counting the fixed arrangements for one cycle length
Assume \(\ell\mid n\). Then the \(d=\frac{mn}{\ell}\) cycles are distinguishable cycle slots, and we must color them so that each of the \(m\) colors is used exactly \(\frac{n}{\ell}\) times.
So the number of fixed arrangements is the multinomial coefficient
$$\mathrm{Fix}(\ell)=\frac{\left(\frac{mn}{\ell}\right)!}{\left(\frac{n}{\ell}!\right)^m}.$$
If \(\ell\nmid n\), then
$$\mathrm{Fix}(\ell)=0.$$
5) How many rotations have the same cycle length?
Now fix a divisor \(\ell\) of \(N\). We want the number of rotations \(r\) whose cycle length is exactly \(\ell\), that is, whose gcd with \(N\) equals \(N/\ell\).
Write
$$r=\frac{N}{\ell}k.$$
Then
$$\gcd(N,r)=\frac{N}{\ell}\gcd(\ell,k).$$
So the condition \(\gcd(N,r)=N/\ell\) is equivalent to
$$\gcd(\ell,k)=1.$$
The number of such residues \(k\) modulo \(\ell\) is exactly Euler's totient:
$$\varphi(\ell).$$
Therefore there are \(\varphi(\ell)\) rotations with cycle length \(\ell\).
6) The divisor formula used by the code
Only divisors \(\ell\mid n\) contribute, so Burnside becomes
$$f(m,n)=\frac{1}{mn}\sum_{\ell\mid n}\varphi(\ell)\, \frac{(mn/\ell)!}{\bigl((n/\ell)!\bigr)^m}.$$
This is exactly the formula implemented by f_value() in the C++ solution.
7) Worked example: \(f(3,2)=16\)
Here \(m=3\), \(n=2\), so \(N=6\). The divisors of \(n\) are \(1\) and \(2\). Hence
$$f(3,2)=\frac{1}{6}\left[ \varphi(1)\frac{6!}{(2!)^3}+ \varphi(2)\frac{3!}{(1!)^3} \right].$$
Now
$$\varphi(1)=1,\qquad \varphi(2)=1,$$
so
$$f(3,2)=\frac{1}{6}\left[\frac{720}{8}+6\right] =\frac{1}{6}(90+6)=16.$$
This is one of the explicit checkpoints in the code.
8) Another small example: \(f(2,3)=4\)
With two colors and three copies of each,
$$f(2,3)=\frac{1}{6}\left[ \varphi(1)\frac{6!}{(3!)^2}+ \varphi(3)\frac{2!}{(1!)^2} \right].$$
Since \(\varphi(1)=1\) and \(\varphi(3)=2\), we get
$$f(2,3)=\frac{1}{6}(20+4)=4.$$
The brute-force checkpoint in the program confirms this value by explicit necklace enumeration.
9) Why the search over \(n\) is finite
The Burnside sum is positive-term, so just the identity rotation \(\ell=1\) already gives a lower bound:
$$f(m,n)\ge \frac{1}{mn}\frac{(mn)!}{(n!)^m}=:g(m,n).$$
For fixed \(n\), this lower bound increases with \(m\). Indeed,
$$\frac{g(m+1,n)}{g(m,n)} =\frac{m}{m+1}\cdot \frac{\prod_{k=1}^{n}(mn+k)}{n!} >1.$$
So among all \(m\ge2\), the smallest possible value occurs at \(m=2\). Therefore
$$f(m,n)\ge g(2,n)=\frac{(2n)!}{2n\,(n!)^2} =\frac{1}{2n}\binom{2n}{n}.$$
If this lower bound already exceeds \(10^{15}\), then no value with that \(n\) can contribute, for any \(m\ge2\).
This is exactly why the code computes
$$\frac{1}{2n}\binom{2n}{n}$$
in compute_n_upper_bound().
10) Why the search over \(m\) is finite
Take \(n=1\). Then we place \(m\) distinct colors once each around the circle, modulo rotation. The number of circular orders is
$$f(m,1)=(m-1)!.$$
So if
$$ (m-1)! > 10^{15}, $$
then even the smallest possible \(n\) already overshoots the limit, and this \(m\) can never contribute.
That is why compute_m_upper_bound() effectively finds the largest \(m\) with
$$ (m-1)! \le 10^{15}. $$
Code Logic
1) Totients. compute_totients() sieves all \(\varphi(\ell)\) values up to the maximum relevant \(n\).
2) Factorials and factorial powers. factorials_up_to() precomputes \(k!\), and precompute_fact_powers() stores \((n!)^m\). This avoids repeating huge multiplications in every Burnside term.
3) Burnside evaluator. f_value(m,n,...) loops over all divisors \(\ell\mid n\), computes the multinomial fixed-count term, multiplies by \(\varphi(\ell)\), sums, and divides by \(mn\).
4) Checkpoints. The code explicitly verifies
$$f(2,1)=1,\qquad f(2,2)=2,\qquad f(3,1)=2,\qquad f(3,2)=16,$$
and then compares the formula with brute-force necklace counting for
$$ (m,n)\in\{(2,3),(2,4),(3,2),(4,2)\}. $$
5) Final scan. After the safe upper bounds are known, the program checks every \((m,n)\) in that rectangle and adds only values \(\le10^{15}\).
Complexity Analysis
For each pair \((m,n)\), the work is proportional to the number of divisors of \(n\), plus big-integer arithmetic on factorial ratios. The precomputed totients, factorials, and factorial powers greatly reduce repeated work. Memory usage is dominated by those precomputed big-integer tables.
Further Reading
- Problem page: https://projecteuler.net/problem=281
- Burnside鈥檚 lemma: https://en.wikipedia.org/wiki/Burnside%27s_lemma
- P贸lya/Burnside counting for necklaces: https://en.wikipedia.org/wiki/P%C3%B3lya_enumeration_theorem
Problem 281 source code
C++
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include <boost/multiprecision/cpp_int.hpp>
namespace {
using i64 = std::int64_t;
using u64 = std::uint64_t;
using boost::multiprecision::cpp_int;
constexpr u64 kLimit = 1000000000000000ULL;
struct Options {
bool run_checkpoints = true;
};
bool parse_arguments(int argc, char** argv, Options& options) {
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--skip-checkpoints") {
options.run_checkpoints = false;
continue;
}
std::cerr << "Unknown argument: " << arg << '\n';
return false;
}
return true;
}
std::vector<int> compute_totients(const int n_max) {
std::vector<int> phi(static_cast<std::size_t>(n_max + 1));
for (int i = 0; i <= n_max; ++i) {
phi[static_cast<std::size_t>(i)] = i;
}
for (int p = 2; p <= n_max; ++p) {
if (phi[static_cast<std::size_t>(p)] != p) {
continue;
}
for (int multiple = p; multiple <= n_max; multiple += p) {
phi[static_cast<std::size_t>(multiple)] -= phi[static_cast<std::size_t>(multiple)] / p;
}
}
return phi;
}
cpp_int central_binomial(const int n) {
cpp_int c = 1;
for (int i = 1; i <= n; ++i) {
c *= (n + i);
c /= i;
}
return c;
}
int compute_n_upper_bound(const u64 limit) {
const cpp_int limit_big = limit;
int n = 1;
int best = 0;
while (true) {
const cpp_int bound = central_binomial(n) / (2 * n);
if (bound > limit_big) {
break;
}
best = n;
++n;
}
return best;
}
int compute_m_upper_bound(const u64 limit) {
const cpp_int limit_big = limit;
cpp_int factorial = 1; // 1! = 1
int best = 1;
for (int m = 2;; ++m) {
if (factorial > limit_big) {
break;
}
best = m;
factorial *= m; // factorial now becomes m!
}
return best;
}
std::vector<cpp_int> factorials_up_to(const int n_max) {
std::vector<cpp_int> fact(static_cast<std::size_t>(n_max + 1));
fact[0] = 1;
for (int i = 1; i <= n_max; ++i) {
fact[static_cast<std::size_t>(i)] = fact[static_cast<std::size_t>(i - 1)] * i;
}
return fact;
}
std::vector<std::vector<cpp_int>> precompute_fact_powers(
const std::vector<cpp_int>& fact,
const int n_max,
const int m_max
) {
std::vector<std::vector<cpp_int>> powers(
static_cast<std::size_t>(n_max + 1),
std::vector<cpp_int>(static_cast<std::size_t>(m_max + 1), cpp_int(1))
);
for (int n = 0; n <= n_max; ++n) {
powers[static_cast<std::size_t>(n)][0] = 1;
for (int m = 1; m <= m_max; ++m) {
powers[static_cast<std::size_t>(n)][static_cast<std::size_t>(m)] =
powers[static_cast<std::size_t>(n)][static_cast<std::size_t>(m - 1)] *
fact[static_cast<std::size_t>(n)];
}
}
return powers;
}
cpp_int f_value(
const int m,
const int n,
const std::vector<int>& phi,
const std::vector<cpp_int>& fact,
const std::vector<std::vector<cpp_int>>& fact_powers
) {
const int total = m * n;
cpp_int sum = 0;
for (int l = 1; l <= n; ++l) {
if ((n % l) != 0) {
continue;
}
const int slice = n / l;
const cpp_int numerator = fact[static_cast<std::size_t>(total / l)];
const cpp_int denominator = fact_powers[static_cast<std::size_t>(slice)][static_cast<std::size_t>(m)];
const cpp_int multinomial = numerator / denominator;
sum += static_cast<cpp_int>(phi[static_cast<std::size_t>(l)]) * multinomial;
}
return sum / total;
}
std::vector<int> rotate_left(const std::vector<int>& v, const int shift) {
const int n = static_cast<int>(v.size());
std::vector<int> out(static_cast<std::size_t>(n));
for (int i = 0; i < n; ++i) {
out[static_cast<std::size_t>(i)] = v[static_cast<std::size_t>((i + shift) % n)];
}
return out;
}
u64 brute_count_necklaces(const int m, const int n) {
std::vector<int> arrangement;
arrangement.reserve(static_cast<std::size_t>(m * n));
for (int color = 0; color < m; ++color) {
for (int cnt = 0; cnt < n; ++cnt) {
arrangement.push_back(color);
}
}
std::set<std::vector<int>> canonical;
std::sort(arrangement.begin(), arrangement.end());
do {
std::vector<int> best = arrangement;
for (int shift = 1; shift < m * n; ++shift) {
std::vector<int> r = rotate_left(arrangement, shift);
if (r < best) {
best = std::move(r);
}
}
canonical.insert(std::move(best));
} while (std::next_permutation(arrangement.begin(), arrangement.end()));
return static_cast<u64>(canonical.size());
}
bool run_checkpoints(
const std::vector<int>& phi,
const std::vector<cpp_int>& fact,
const std::vector<std::vector<cpp_int>>& fact_powers
) {
struct Case {
int m;
int n;
u64 expected;
};
const std::vector<Case> explicit_cases = {
{2, 1, 1},
{2, 2, 2},
{3, 1, 2},
{3, 2, 16},
};
for (const Case& c : explicit_cases) {
const cpp_int value = f_value(c.m, c.n, phi, fact, fact_powers);
if (value != c.expected) {
std::cerr << "Explicit checkpoint failed for f(" << c.m << ',' << c.n
<< "): got " << value << ", expected " << c.expected << '\n';
return false;
}
}
const std::vector<std::pair<int, int>> brute_cases = {
{2, 3},
{2, 4},
{3, 2},
{4, 2},
};
for (const auto& [m, n] : brute_cases) {
const cpp_int formula = f_value(m, n, phi, fact, fact_powers);
const u64 brute = brute_count_necklaces(m, n);
if (formula != brute) {
std::cerr << "Brute checkpoint failed for f(" << m << ',' << n
<< "): formula=" << formula << ", brute=" << brute << '\n';
return false;
}
}
return true;
}
u64 solve() {
const int n_upper = compute_n_upper_bound(kLimit);
const int m_upper = compute_m_upper_bound(kLimit);
const int max_factorial_needed = m_upper * n_upper;
const std::vector<cpp_int> fact = factorials_up_to(max_factorial_needed);
const std::vector<int> phi = compute_totients(n_upper);
const std::vector<std::vector<cpp_int>> fact_powers = precompute_fact_powers(fact, n_upper, m_upper);
if (!run_checkpoints(phi, fact, fact_powers)) {
return 0;
}
const cpp_int limit_big = kLimit;
u64 answer = 0;
for (int m = 2; m <= m_upper; ++m) {
for (int n = 1; n <= n_upper; ++n) {
const cpp_int value = f_value(m, n, phi, fact, fact_powers);
if (value <= limit_big) {
answer += value.convert_to<u64>();
}
}
}
return answer;
}
} // namespace
int main(int argc, char** argv) {
Options options;
if (!parse_arguments(argc, argv, options)) {
return 1;
}
const int n_upper = compute_n_upper_bound(kLimit);
const int m_upper = compute_m_upper_bound(kLimit);
const int max_factorial_needed = m_upper * n_upper;
const std::vector<cpp_int> fact = factorials_up_to(max_factorial_needed);
const std::vector<int> phi = compute_totients(n_upper);
const std::vector<std::vector<cpp_int>> fact_powers = precompute_fact_powers(fact, n_upper, m_upper);
if (options.run_checkpoints && !run_checkpoints(phi, fact, fact_powers)) {
return 2;
}
const cpp_int limit_big = kLimit;
u64 answer = 0;
for (int m = 2; m <= m_upper; ++m) {
for (int n = 1; n <= n_upper; ++n) {
const cpp_int value = f_value(m, n, phi, fact, fact_powers);
if (value <= limit_big) {
answer += value.convert_to<u64>();
}
}
}
std::cout << answer << '\n';
return 0;
}
Python
import math
def compute_totients(n_max):
phi = list(range(n_max + 1))
for p in range(2, n_max + 1):
if phi[p] == p:
for multiple in range(p, n_max + 1, p):
phi[multiple] -= phi[multiple] // p
return phi
def central_binomial(n):
return math.comb(2 * n, n)
def compute_n_upper_bound(limit):
n = 1
best = 0
while True:
bound = central_binomial(n) // (2 * n)
if bound > limit:
break
best = n
n += 1
return best
def compute_m_upper_bound(limit):
factorial = 1
best = 1
m = 2
while True:
if factorial > limit:
break
best = m
factorial *= m
m += 1
return best
def f_value(m, n, phi, fact, fact_powers):
total = m * n
sum_val = 0
for l in range(1, n + 1):
if n % l != 0:
continue
slice_len = n // l
numerator = fact[total // l]
denominator = fact_powers[slice_len][m]
multinomial = numerator // denominator
sum_val += phi[l] * multinomial
return sum_val // total
def solve(limit=1000000000000000):
n_upper = compute_n_upper_bound(limit)
m_upper = compute_m_upper_bound(limit)
max_factorial_needed = m_upper * n_upper
fact = [1] * (max_factorial_needed + 1)
for i in range(1, max_factorial_needed + 1):
fact[i] = fact[i - 1] * i
phi = compute_totients(n_upper)
fact_powers = [[1] * (m_upper + 1) for _ in range(n_upper + 1)]
for n in range(n_upper + 1):
fact_powers[n][0] = 1
for m in range(1, m_upper + 1):
fact_powers[n][m] = fact_powers[n][m - 1] * fact[n]
answer = 0
for m in range(2, m_upper + 1):
for n in range(1, n_upper + 1):
val = f_value(m, n, phi, fact, fact_powers)
if val <= limit:
answer += val
return str(answer)
if __name__ == '__main__':
print(solve())
Java
import java.math.BigInteger;
public class Euler281 {
static final long LIMIT = 1000000000000000L;
static int[] computeTotients(int nMax) {
int[] phi = new int[nMax + 1];
for (int i = 0; i <= nMax; ++i)
phi[i] = i;
for (int p = 2; p <= nMax; ++p) {
if (phi[p] == p) {
for (int multiple = p; multiple <= nMax; multiple += p) {
phi[multiple] -= phi[multiple] / p;
}
}
}
return phi;
}
static BigInteger centralBinomial(int n) {
BigInteger c = BigInteger.ONE;
for (int i = 1; i <= n; ++i) {
c = c.multiply(BigInteger.valueOf(n + i))
.divide(BigInteger.valueOf(i));
}
return c;
}
static int computeNUpperBound(long limit) {
BigInteger limitBig = BigInteger.valueOf(limit);
int n = 1;
int best = 0;
while (true) {
BigInteger bound = centralBinomial(n).divide(BigInteger.valueOf(2L * n));
if (bound.compareTo(limitBig) > 0)
break;
best = n;
++n;
}
return best;
}
static int computeMUpperBound(long limit) {
BigInteger limitBig = BigInteger.valueOf(limit);
BigInteger factorial = BigInteger.ONE;
int best = 1;
for (int m = 2;; ++m) {
if (factorial.compareTo(limitBig) > 0)
break;
best = m;
factorial = factorial.multiply(BigInteger.valueOf(m));
}
return best;
}
static BigInteger[] factorialsUpTo(int nMax) {
BigInteger[] fact = new BigInteger[nMax + 1];
fact[0] = BigInteger.ONE;
for (int i = 1; i <= nMax; ++i) {
fact[i] = fact[i - 1].multiply(BigInteger.valueOf(i));
}
return fact;
}
static BigInteger[][] precomputeFactPowers(BigInteger[] fact, int nMax, int mMax) {
BigInteger[][] powers = new BigInteger[nMax + 1][mMax + 1];
for (int n = 0; n <= nMax; ++n) {
powers[n][0] = BigInteger.ONE;
for (int m = 1; m <= mMax; ++m) {
powers[n][m] = powers[n][m - 1].multiply(fact[n]);
}
}
return powers;
}
static BigInteger fValue(int m, int n, int[] phi, BigInteger[] fact, BigInteger[][] factPowers) {
int total = m * n;
BigInteger sum = BigInteger.ZERO;
for (int l = 1; l <= n; ++l) {
if (n % l != 0)
continue;
int slice = n / l;
BigInteger numerator = fact[total / l];
BigInteger denominator = factPowers[slice][m];
BigInteger multinomial = numerator.divide(denominator);
sum = sum.add(multinomial.multiply(BigInteger.valueOf(phi[l])));
}
return sum.divide(BigInteger.valueOf(total));
}
public static String solve() {
int nUpper = computeNUpperBound(LIMIT);
int mUpper = computeMUpperBound(LIMIT);
int maxFactorialNeeded = mUpper * nUpper;
BigInteger[] fact = factorialsUpTo(maxFactorialNeeded);
int[] phi = computeTotients(nUpper);
BigInteger[][] factPowers = precomputeFactPowers(fact, nUpper, mUpper);
BigInteger limitBig = BigInteger.valueOf(LIMIT);
long answer = 0;
for (int m = 2; m <= mUpper; ++m) {
for (int n = 1; n <= nUpper; ++n) {
BigInteger value = fValue(m, n, phi, fact, factPowers);
if (value.compareTo(limitBig) <= 0) {
answer += value.longValue();
}
}
}
return String.valueOf(answer);
}
public static void main(String[] args) {
System.out.println(solve());
}
}