Project Euler Problem 118

Using all of the digits 1 through 9 and concatenating them freely to form decimal integers, different sets can be formed

Project Euler Problem 118

Solution

Answer: 44680

We must count the number of sets of primes formed by using each digit $1,\dots,9$ exactly once.

For example,

$${2,5,47,89,631}$$

is valid because:

  • every element is prime,
  • the digits $1$ through $9$ are used exactly once overall,
  • order does not matter because this is a set.

The challenge is to count all such distinct sets.


Mathematical analysis

1. Observations about primes

We concatenate digits to form integers.

A number formed from distinct digits can only be prime if:

  • it does not end in an even digit,
  • it does not end in $5$,
  • it is not divisible by $3$.

The divisibility-by-3 observation is especially important.

For any integer, divisibility by $3$ depends on the sum of digits.

The digits $1$ through $9$ sum to

$$1+2+\cdots+9 = 45.$$

Since $45\equiv 0 \pmod 3$, the entire concatenation is divisible by $3$.

Therefore we cannot place all digits into one prime.

More generally:

  • any subset of digits whose digit sum is divisible by $3$ cannot form a prime (except the number $3$ itself).

This dramatically prunes the search space.


2. Strategy

We will:

  1. Generate every prime that:
  • uses distinct digits from $1$–$9$,
  • uses no digit twice,
  • contains no zero.
  1. Represent each prime by a bitmask of used digits.
  2. Use recursive search/backtracking to combine disjoint masks until all nine digits are used.
  3. Enforce an ordering condition to avoid counting the same set multiple times.

3. Prime generation

There are only

$$\sum_{k=1}^9 P(9,k)$$

possible digit arrangements, where

$$P(9,k)=\frac{9!}{(9-k)!}.$$

This total is manageable.

For each permutation of length $k$:

  • convert to integer,
  • test primality,
  • store if prime.

4. Avoiding duplicate sets

Suppose we recursively choose primes.

If we allow arbitrary order, then

$${2,5,47}$$

would be counted as:

  • $2,5,47$
  • $2,47,5$
  • $5,2,47$
  • etc.

To prevent this, we require primes to be selected in increasing order.

Thus each set is generated exactly once.


Python implementation

from itertools import permutations
from math import isqrt

# ------------------------------------------------------------
# Primality test
# ------------------------------------------------------------

def is_prime(n):
    """Return True if n is prime."""
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2
    if n % 3 == 0:
        return n == 3

    r = isqrt(n)
    f = 5
    while f <= r:
        if n % f == 0 or n % (f + 2) == 0:
            return False
        f += 6
    return True

# ------------------------------------------------------------
# Generate all usable primes
# ------------------------------------------------------------

digits = '123456789'

# primes_by_mask[mask] = list of primes using exactly those digits
primes = []

for length in range(1, 10):
    for perm in permutations(digits, length):

        # quick divisibility-by-3 pruning
        digit_sum = sum(ord(c) - ord('0') for c in perm)
        if digit_sum % 3 == 0:
            # only possible prime is 3 itself
            if not (length == 1 and perm[0] == '3'):
                continue

        # last digit cannot be even or 5
        if perm[-1] in '24568':
            if not (length == 1 and perm[0] in '25'):
                continue

        n = int(''.join(perm))

        if is_prime(n):

            # build bitmask
            mask = 0
            for ch in perm:
                mask |= 1 << (ord(ch) - ord('1'))

            primes.append((n, mask))

# ------------------------------------------------------------
# Sort primes so recursive generation is canonical
# ------------------------------------------------------------

primes.sort()

FULL_MASK = (1 << 9) - 1

answer = 0

# ------------------------------------------------------------
# Backtracking search
# ------------------------------------------------------------

def search(start_index, used_mask):
    """
    Choose increasing primes whose digit masks are disjoint
    until all digits are used.
    """
    global answer

    if used_mask == FULL_MASK:
        answer += 1
        return

    for i in range(start_index, len(primes)):
        p, mask = primes[i]

        # disjoint digit sets only
        if used_mask & mask:
            continue

        search(i + 1, used_mask | mask)

search(0, 0)

print(answer)

Code walkthrough

Primality testing

def is_prime(n):

We use trial division up to $\sqrt n$, optimized with the $6k\pm1$ pattern.


Generating permutations

for length in range(1, 10):
    for perm in permutations(digits, length):

This enumerates every possible number using distinct digits.


Divisibility pruning

digit_sum = ...
if digit_sum % 3 == 0:

If the digit sum is divisible by $3$, the number is divisible by $3$.

The only exception is the single-digit prime $3$.


Last-digit pruning

if perm[-1] in '24568':

Any multi-digit prime ending in an even digit or $5$ is composite.


Bitmask encoding

mask |= 1 << (ord(ch) - ord('1'))

Each digit corresponds to one bit:

  • digit 1 → bit 0,
  • digit 2 → bit 1,
  • ...
  • digit 9 → bit 8.

This lets us test overlap quickly using bit operations.


if used_mask & mask:
    continue

Skip primes sharing digits with already-used primes.


Canonical ordering

search(i + 1, ...)

We only recurse forward through the sorted list, ensuring each set is counted once.


Small-example verification

The example set

$${2,5,47,89,631}$$

works because:

  • all are prime,
  • masks are disjoint,
  • together they cover all digits $1$–$9$.

The recursion would count it exactly once due to increasing order.


Final verification

The count should:

  • be fairly large because many partitions exist,
  • but not astronomically huge because primality constraints are strong,
  • definitely be an integer well below the total number of permutations.

Running the program gives:

$$44680$$

Therefore,

Answer: 44680