Problem 516: $5$-smooth Totients

View on Project Euler

Project Euler Problem 516 Solution

EulerSolve provides an optimized solution for Project Euler Problem 516, $5$-smooth Totients, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For a given limit \(L\), define \(S(L)\) as the sum of all integers \(n\le L\) such that Euler's totient \(\varphi(n)\) is 5-smooth, meaning that every prime factor of \(\varphi(n)\) belongs to \(\{2,3,5\}\). The target value is \(S(10^{12}) \bmod 2^{32}\), so a direct scan of all \(n\le 10^{12}\) is completely infeasible. Mathematical Approach The key observation is that the condition on \(\varphi(n)\) forces a very rigid prime factorization for \(n\). Once that structure is identified, the sum can be reorganized into a much smaller search over 5-smooth numbers and a recursively generated family of admissible prime products. Step 1: Separate the \(2,3,5\) Part from the Larger Primes Write $$n=2^a3^b5^c\prod_{i=1}^r p_i^{e_i},\qquad p_i>5.$$ Euler's product formula gives $$\varphi(n)=\varphi(2^a)\varphi(3^b)\varphi(5^c)\prod_{i=1}^r p_i^{e_i-1}(p_i-1).$$ The factors coming from \(2,3,5\) are always 5-smooth: $$\varphi(2^a)\in\{1,2,4,8,\dots\},\qquad \varphi(3^b)\in\{1,2,6,18,\dots\},\qquad \varphi(5^c)\in\{1,4,20,100,\dots\}.$$ So every possible obstruction comes from primes \(p_i>5\). Step 2: Characterize Which Large Primes Are Allowed If some prime \(p_i>5\) appears with exponent \(e_i\ge 2\), then the factor \(p_i^{e_i-1}\) divides \(\varphi(n)\). That would force a prime factor larger than \(5\) into \(\varphi(n)\), which is impossible....

Detailed mathematical approach

Problem Summary

For a given limit \(L\), define \(S(L)\) as the sum of all integers \(n\le L\) such that Euler's totient \(\varphi(n)\) is 5-smooth, meaning that every prime factor of \(\varphi(n)\) belongs to \(\{2,3,5\}\). The target value is \(S(10^{12}) \bmod 2^{32}\), so a direct scan of all \(n\le 10^{12}\) is completely infeasible.

Mathematical Approach

The key observation is that the condition on \(\varphi(n)\) forces a very rigid prime factorization for \(n\). Once that structure is identified, the sum can be reorganized into a much smaller search over 5-smooth numbers and a recursively generated family of admissible prime products.

Step 1: Separate the \(2,3,5\) Part from the Larger Primes

Write

$$n=2^a3^b5^c\prod_{i=1}^r p_i^{e_i},\qquad p_i>5.$$

Euler's product formula gives

$$\varphi(n)=\varphi(2^a)\varphi(3^b)\varphi(5^c)\prod_{i=1}^r p_i^{e_i-1}(p_i-1).$$

The factors coming from \(2,3,5\) are always 5-smooth:

$$\varphi(2^a)\in\{1,2,4,8,\dots\},\qquad \varphi(3^b)\in\{1,2,6,18,\dots\},\qquad \varphi(5^c)\in\{1,4,20,100,\dots\}.$$

So every possible obstruction comes from primes \(p_i>5\).

Step 2: Characterize Which Large Primes Are Allowed

If some prime \(p_i>5\) appears with exponent \(e_i\ge 2\), then the factor \(p_i^{e_i-1}\) divides \(\varphi(n)\). That would force a prime factor larger than \(5\) into \(\varphi(n)\), which is impossible. Therefore every prime \(p>5\) may appear in \(n\) at most once.

Moreover, when \(p>5\) does appear, the factor \(p-1\) divides \(\varphi(n)\). Hence \(p-1\) itself must be 5-smooth. This proves that every valid number has the shape

$$n=hq,$$

where \(h=2^a3^b5^c\) is a 5-smooth number and \(q\) is a squarefree product of distinct primes \(p>5\) satisfying

$$p-1=2^\alpha 3^\beta 5^\gamma,\qquad \alpha,\beta,\gamma\ge 0.$$

The converse is also true. If \(h\) is 5-smooth and \(q=\prod p\) is a squarefree product of distinct primes with \(p-1\) 5-smooth, then \(h\) and \(q\) are coprime, so

$$\varphi(hq)=\varphi(h)\prod_{p\mid q}(p-1),$$

which is again 5-smooth. Thus the decomposition is exact.

Step 3: Turn the Problem into a Sum over Admissible Prime Products

Let

$$\mathcal{H}(x)=\left\{2^a3^b5^c\le x:\ a,b,c\ge 0\right\}$$

be the set of 5-smooth numbers up to \(x\), and define the prefix-sum function

$$A(x)=\sum_{h\in\mathcal{H}(x)} h.$$

Also define the admissible primes

$$\mathcal{P}(L)=\left\{p\le L:\ p>5,\ p\text{ prime},\ p-1\in\mathcal{H}(L)\right\}.$$

Every valid \(n\le L\) is uniquely obtained by choosing a squarefree product \(q\) of distinct primes from \(\mathcal{P}(L)\), then choosing a 5-smooth multiplier \(h\le L/q\). Therefore

$$S(L)=\sum_{q\in\mathcal{Q}(L)} \sum_{\substack{h\in\mathcal{H}(L)\\ h\le L/q}} qh,$$

where \(\mathcal{Q}(L)\) is the set of squarefree products \(q\le L\) built from distinct primes in \(\mathcal{P}(L)\), including the empty product \(q=1\).

Pulling \(q\) outside the inner sum gives the main formula

$$\boxed{S(L)=\sum_{q\in\mathcal{Q}(L)} q\,A\left(\left\lfloor\frac{L}{q}\right\rfloor\right)\pmod{2^{32}}.}$$

Step 4: Why Enumerating \(h+1\) Is Enough

The previous step shows that admissible primes are exactly the primes of the form

$$p=h+1,\qquad h\in\mathcal{H}(L).$$

So instead of searching through all primes up to \(L\), we only generate 5-smooth numbers and test \(h+1\) for primality. This is the decisive reduction: the 5-smooth list is tiny compared with the interval \([1,L]\).

After sorting the 5-smooth numbers, the values \(A(x)\) can be answered by binary search and prefix sums. The remaining task is then to enumerate all feasible subset products \(q\in\mathcal{Q}(L)\), which is done naturally by depth-first search with the pruning rule \(q p\le L\).

Worked Example: \(S(100)=3728\)

For \(L=100\), the 5-smooth numbers are

$$\{1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36,40,45,48,50,54,60,64,72,75,80,81,90,96,100\}.$$

The admissible primes \(p=h+1\) are

$$\{7,11,13,17,19,31,37,41,61,73,97\}.$$

Among their squarefree products, the only ones not exceeding \(100\) are

$$1,\ 7,\ 11,\ 13,\ 17,\ 19,\ 31,\ 37,\ 41,\ 61,\ 73,\ 97,\ 77,\ 91.$$

The needed prefix sums are

$$A(100)=1258,\quad A(14)=60,\quad A(9)=38,\quad A(7)=21,\quad A(5)=15,\quad A(3)=6,\quad A(2)=3,\quad A(1)=1.$$

So the contributions are

$$\begin{aligned} q=1&:&&1\cdot A(100)=1258,\\ q=7&:&&7\cdot A(14)=420,\\ q=11&:&&11\cdot A(9)=418,\\ q=13&:&&13\cdot A(7)=273,\\ q=17&:&&17\cdot A(5)=255,\\ q=19&:&&19\cdot A(5)=285,\\ q=31&:&&31\cdot A(3)=186,\\ q=37&:&&37\cdot A(2)=111,\\ q=41&:&&41\cdot A(2)=123,\\ q=61&:&&61\cdot A(1)=61,\\ q=73&:&&73\cdot A(1)=73,\\ q=97&:&&97\cdot A(1)=97,\\ q=77&:&&77\cdot A(1)=77,\\ q=91&:&&91\cdot A(1)=91. \end{aligned}$$

Adding them gives

$$1258+420+418+273+255+285+186+111+123+61+73+97+77+91=3728,$$

which matches the checkpoint used by the implementation.

How the Code Works

The C++, Python, and Java implementations all use the same algorithm.

First, they generate every number of the form \(2^a3^b5^c\le L\) by nested multiplicative loops. The resulting list is sorted, duplicates are removed, and a prefix-sum array modulo \(2^{32}\) is built so that \(A(x)\) can be queried quickly.

Next, for each 5-smooth number \(h\), the implementation tests whether \(h+1\) is prime. It uses fast modular exponentiation and a Miller-Rabin primality test with fixed small bases, which is sufficient for the numeric range of this problem. Every prime \(h+1>5\) is stored as an admissible prime.

Finally, a depth-first search enumerates subset products \(q\) of admissible primes in increasing order. At each recursive state it adds

$$q\,A\left(\left\lfloor\frac{L}{q}\right\rfloor\right)\pmod{2^{32}}$$

to the answer, then tries to append later admissible primes as long as the product stays at most \(L\). Because the search only moves forward through the sorted prime list, each squarefree subset is visited exactly once.

Complexity Analysis

Let \(H=|\mathcal{H}(L)|\), let \(P=|\mathcal{P}(L)|\), and let \(T=|\mathcal{Q}(L)|\), the number of subset products actually visited by the depth-first search. Generating the 5-smooth list takes \(O(H)\) multiplicative steps, while sorting and deduplicating costs \(O(H\log H)\). Building prefix sums is linear in \(H\).

Primality is tested only on the \(H\) candidates \(h+1\), not on every integer up to \(L\). With a fixed-base Miller-Rabin test, this contributes roughly \(O(H\log L)\) modular-arithmetic work. The depth-first search visits each feasible subset product once and performs one binary search on the 5-smooth list per node, so the summation phase costs \(O(T\log H)\).

Thus the algorithm is dominated by the sizes of the 5-smooth list and the feasible subset-product tree, both of which are tiny compared with \(L\) itself. Memory usage is \(O(H+P)\), plus recursion depth proportional to the number of primes currently selected.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=516
  2. Euler's totient function: Wikipedia — Euler's totient function
  3. Regular numbers / 5-smooth numbers: Wikipedia — Regular number
  4. Miller-Rabin primality test: Wikipedia — Miller-Rabin primality test
  5. Squarefree integers: Wikipedia — Square-free integer

Problem 516 source code

C++

#include <algorithm>
#include <cstdint>
#include <functional>
#include <iostream>
#include <vector>

namespace {

using u64 = std::uint64_t;
using u128 = __uint128_t;

constexpr u64 kMask32 = 0xFFFF'FFFFULL;

u64 mod_mul(const u64 a, const u64 b, const u64 mod) {
    return static_cast<u64>((static_cast<u128>(a) * b) % mod);
}

u64 mod_pow(u64 base, u64 exp, const u64 mod) {
    u64 result = 1ULL % mod;
    u64 cur = base % mod;
    u64 e = exp;
    while (e > 0ULL) {
        if (e & 1ULL) {
            result = mod_mul(result, cur, mod);
        }
        cur = mod_mul(cur, cur, mod);
        e >>= 1ULL;
    }
    return result;
}

bool is_prime(const u64 n) {
    if (n < 2ULL) {
        return false;
    }
    for (const u64 p : {2ULL, 3ULL, 5ULL, 7ULL, 11ULL, 13ULL}) {
        if (n == p) {
            return true;
        }
        if (n % p == 0ULL) {
            return false;
        }
    }

    u64 d = n - 1ULL;
    int s = 0;
    while ((d & 1ULL) == 0ULL) {
        d >>= 1ULL;
        ++s;
    }

    for (const u64 a : {2ULL, 3ULL, 5ULL, 7ULL, 11ULL, 13ULL}) {
        if (a >= n) {
            continue;
        }
        u64 x = mod_pow(a, d, n);
        if (x == 1ULL || x == n - 1ULL) {
            continue;
        }
        bool witness = true;
        for (int r = 1; r < s; ++r) {
            x = mod_mul(x, x, n);
            if (x == n - 1ULL) {
                witness = false;
                break;
            }
        }
        if (witness) {
            return false;
        }
    }
    return true;
}

std::vector<u64> generate_hamming(const u64 limit) {
    std::vector<u64> out;
    for (u64 a = 1ULL; a <= limit; a *= 2ULL) {
        for (u64 b = a; b <= limit; b *= 3ULL) {
            for (u64 c = b; c <= limit; c *= 5ULL) {
                out.push_back(c);
                if (c > limit / 5ULL) {
                    break;
                }
            }
            if (b > limit / 3ULL) {
                break;
            }
        }
        if (a > limit / 2ULL) {
            break;
        }
    }
    std::sort(out.begin(), out.end());
    out.erase(std::unique(out.begin(), out.end()), out.end());
    return out;
}

u64 solve(const u64 limit) {
    const std::vector<u64> hamming = generate_hamming(limit);

    std::vector<u64> prefix_mod(static_cast<std::size_t>(hamming.size() + 1), 0ULL);
    for (std::size_t i = 0; i < hamming.size(); ++i) {
        prefix_mod[i + 1] = (prefix_mod[i] + (hamming[i] & kMask32)) & kMask32;
    }

    std::vector<u64> admissible_primes;
    admissible_primes.reserve(hamming.size());
    for (const u64 h : hamming) {
        const u64 p = h + 1ULL;
        if (p > 5ULL && p <= limit && is_prime(p)) {
            admissible_primes.push_back(p);
        }
    }
    std::sort(admissible_primes.begin(), admissible_primes.end());

    u64 total_mod = 0ULL;

    const auto sum_hamming_mod = [&](const u64 x) -> u64 {
        const auto it = std::upper_bound(hamming.begin(), hamming.end(), x);
        const std::size_t count = static_cast<std::size_t>(it - hamming.begin());
        return prefix_mod[count];
    };

    std::function<void(std::size_t, u64)> dfs = [&](const std::size_t idx, const u64 prod) {
        const u64 h_sum = sum_hamming_mod(limit / prod);
        total_mod = (total_mod + ((prod & kMask32) * h_sum & kMask32)) & kMask32;

        for (std::size_t i = idx; i < admissible_primes.size(); ++i) {
            const u64 p = admissible_primes[i];
            if (prod > limit / p) {
                break;
            }
            dfs(i + 1, prod * p);
        }
    };

    dfs(0, 1ULL);
    return total_mod;
}

u64 brute(const int limit) {
    std::vector<int> phi(static_cast<std::size_t>(limit + 1), 0);
    for (int i = 0; i <= limit; ++i) {
        phi[static_cast<std::size_t>(i)] = i;
    }
    for (int p = 2; p <= limit; ++p) {
        if (phi[static_cast<std::size_t>(p)] != p) {
            continue;
        }
        for (int m = p; m <= limit; m += p) {
            phi[static_cast<std::size_t>(m)] -= phi[static_cast<std::size_t>(m)] / p;
        }
    }

    auto is_smooth = [](int x) -> bool {
        while (x % 2 == 0) {
            x /= 2;
        }
        while (x % 3 == 0) {
            x /= 3;
        }
        while (x % 5 == 0) {
            x /= 5;
        }
        return x == 1;
    };

    u64 sum_mod = 0ULL;
    for (int n = 1; n <= limit; ++n) {
        if (is_smooth(phi[static_cast<std::size_t>(n)])) {
            sum_mod = (sum_mod + static_cast<u64>(n)) & kMask32;
        }
    }
    return sum_mod;
}

bool run_checkpoints() {
    if (solve(100ULL) != 3'728ULL) {
        std::cerr << "Checkpoint failed: S(100)\n";
        return false;
    }
    if (solve(5'000ULL) != brute(5'000)) {
        std::cerr << "Checkpoint failed: formula/bruteforce mismatch at 5000\n";
        return false;
    }
    return true;
}

}  // namespace

int main() {
    if (!run_checkpoints()) {
        return 1;
    }

    constexpr u64 limit = 1'000'000'000'000ULL;
    std::cout << solve(limit) << '\n';
    return 0;
}

Python

import math

def solve():
    LIMIT = 10**12
    MASK32 = 0xFFFFFFFF

    def mod_mul(a, b, mod):
        return a * b % mod

    def mod_pow(base, exp, mod):
        result = 1 % mod
        cur = base % mod
        while exp > 0:
            if exp & 1: result = mod_mul(result, cur, mod)
            cur = mod_mul(cur, cur, mod)
            exp >>= 1
        return result

    def is_prime(n):
        if n < 2: return False
        for p in [2, 3, 5, 7, 11, 13]:
            if n == p: return True
            if n % p == 0: return False
        d = n - 1
        s = 0
        while d % 2 == 0: d >>= 1; s += 1
        for a in [2, 3, 5, 7, 11, 13]:
            if a >= n: continue
            x = mod_pow(a, d, n)
            if x == 1 or x == n - 1: continue
            witness = True
            for _ in range(1, s):
                x = mod_mul(x, x, n)
                if x == n - 1: witness = False; break
            if witness: return False
        return True

    def gen_hamming(limit):
        out = []
        a = 1
        while a <= limit:
            b = a
            while b <= limit:
                c = b
                while c <= limit:
                    out.append(c)
                    if c > limit // 5: break
                    c *= 5
                if b > limit // 3: break
                b *= 3
            if a > limit // 2: break
            a *= 2
        out = sorted(set(out))
        return out

    hamming = gen_hamming(LIMIT)
    prefix_mod = [0] * (len(hamming) + 1)
    for i in range(len(hamming)):
        prefix_mod[i+1] = (prefix_mod[i] + (hamming[i] & MASK32)) & MASK32

    admissible = sorted(p for h in hamming if (p := h + 1) > 5 and p <= LIMIT and is_prime(p))

    import bisect
    def sum_hamming_mod(x):
        cnt = bisect.bisect_right(hamming, x)
        return prefix_mod[cnt]

    total_mod = 0
    def dfs(idx, prod):
        nonlocal total_mod
        h_sum = sum_hamming_mod(LIMIT // prod)
        total_mod = (total_mod + ((prod & MASK32) * h_sum & MASK32)) & MASK32
        for i in range(idx, len(admissible)):
            p = admissible[i]
            if prod > LIMIT // p: break
            dfs(i + 1, prod * p)

    dfs(0, 1)
    return str(total_mod)

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

Java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Euler516 {

    static long modPow(long base, long exp, long mod) {
        long result = 1 % mod;
        long cur = base % mod;
        long e = exp;
        while (e > 0) {
            if ((e & 1) == 1) {
                result = modMul(result, cur, mod);
            }
            cur = modMul(cur, cur, mod);
            e >>= 1;
        }
        return result;
    }

    static long modMul(long a, long b, long mod) {
        long q = (long) ((double) a * b / mod);
        long r = a * b - q * mod;
        while (r < 0)
            r += mod;
        while (r >= mod)
            r -= mod;
        return r;
    }

    static boolean isPrime(long n) {
        if (n < 2)
            return false;
        long[] p_list = { 2, 3, 5, 7, 11, 13 };
        for (long p : p_list) {
            if (n == p)
                return true;
            if (n % p == 0)
                return false;
        }

        long d = n - 1;
        int s = 0;
        while ((d & 1) == 0) {
            d >>= 1;
            s++;
        }

        for (long a : p_list) {
            if (a >= n)
                break;
            long x = modPow(a, d, n);
            if (x == 1 || x == n - 1)
                continue;
            boolean witness = true;
            for (int r = 1; r < s; r++) {
                x = modMul(x, x, n);
                if (x == n - 1) {
                    witness = false;
                    break;
                }
            }
            if (witness)
                return false;
        }
        return true;
    }

    static List<Long> generateHamming(long limit) {
        List<Long> out = new ArrayList<>();
        for (long a = 1; a <= limit; a *= 2) {
            for (long b = a; b <= limit; b *= 3) {
                for (long c = b; c <= limit; c *= 5) {
                    out.add(c);
                    if (c > limit / 5)
                        break;
                }
                if (b > limit / 3)
                    break;
            }
            if (a > limit / 2)
                break;
        }
        Collections.sort(out);
        List<Long> uniqueOut = new ArrayList<>();
        if (!out.isEmpty()) {
            uniqueOut.add(out.get(0));
            for (int i = 1; i < out.size(); i++) {
                if (!out.get(i).equals(out.get(i - 1))) {
                    uniqueOut.add(out.get(i));
                }
            }
        }
        return uniqueOut;
    }

    static final long kMask32 = 0xFFFFFFFFL;
    static long totalMod = 0;
    static long limit = 1000000000000L;
    static List<Long> hamming;
    static long[] prefixMod;
    static List<Long> admissiblePrimes;

    static long sumHammingMod(long x) {
        int low = 0, high = hamming.size();
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (hamming.get(mid) <= x) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return prefixMod[low];
    }

    static void dfs(int idx, long prod) {
        long hSum = sumHammingMod(limit / prod);
        totalMod = (totalMod + ((prod & kMask32) * hSum & kMask32)) & kMask32;

        for (int i = idx; i < admissiblePrimes.size(); i++) {
            long p = admissiblePrimes.get(i);
            if (prod > limit / p)
                break;
            dfs(i + 1, prod * p);
        }
    }

    public static void main(String[] args) {
        hamming = generateHamming(limit);
        prefixMod = new long[hamming.size() + 1];
        for (int i = 0; i < hamming.size(); i++) {
            prefixMod[i + 1] = (prefixMod[i] + (hamming.get(i) & kMask32)) & kMask32;
        }

        admissiblePrimes = new ArrayList<>();
        for (long h : hamming) {
            long p = h + 1;
            if (p > 5 && p <= limit && isPrime(p)) {
                admissiblePrimes.add(p);
            }
        }
        Collections.sort(admissiblePrimes);

        dfs(0, 1);

        System.out.println(totalMod);
    }
}