Problem 718: Unreachable Numbers

View on Project Euler

Project Euler Problem 718 Solution

EulerSolve provides an optimized solution for Project Euler Problem 718, Unreachable Numbers, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(a=17^p\), \(b=19^p\), and \(c=23^p\). The task is to compute the sum of all positive integers that cannot be written in the form $$n=ax+by+cz,\qquad x,y,z\in \mathbb{Z}_{>0},$$ for the target case \(p=6\), with the final result reduced modulo \(10^9+7\). Since \(a\), \(b\), and \(c\) are pairwise coprime, there are only finitely many unreachable integers, so the problem becomes a finite numerical-semigroup calculation rather than an infinite search. Mathematical Approach The solution converts the positive-coefficient representation problem into a statement about gaps of a numerical semigroup. Those gaps are then recovered from the Apéry set with respect to the smallest generator. Step 1: Shift from positive coefficients to a semigroup Define the numerical semigroup $$S=\langle a,b,c\rangle=\{ia+jb+kc:i,j,k\in \mathbb{Z}_{\ge 0}\}.$$ Also let $$s=a+b+c.$$ If \(x,y,z\ge 1\), then $$ax+by+cz=s+\bigl(a(x-1)+b(y-1)+c(z-1)\bigr).$$ So a positive integer \(n\) is representable with all three coefficients positive exactly when $$n-s\in S.$$ This means the unreachable positive integers consist of two parts: $$1,2,\dots,s-1,$$ and all integers of the form $$s+g,$$ where \(g\) is a gap of \(S\), meaning \(g\notin S\)....

Detailed mathematical approach

Problem Summary

Let \(a=17^p\), \(b=19^p\), and \(c=23^p\). The task is to compute the sum of all positive integers that cannot be written in the form

$$n=ax+by+cz,\qquad x,y,z\in \mathbb{Z}_{>0},$$

for the target case \(p=6\), with the final result reduced modulo \(10^9+7\). Since \(a\), \(b\), and \(c\) are pairwise coprime, there are only finitely many unreachable integers, so the problem becomes a finite numerical-semigroup calculation rather than an infinite search.

Mathematical Approach

The solution converts the positive-coefficient representation problem into a statement about gaps of a numerical semigroup. Those gaps are then recovered from the Apéry set with respect to the smallest generator.

Step 1: Shift from positive coefficients to a semigroup

Define the numerical semigroup

$$S=\langle a,b,c\rangle=\{ia+jb+kc:i,j,k\in \mathbb{Z}_{\ge 0}\}.$$

Also let

$$s=a+b+c.$$

If \(x,y,z\ge 1\), then

$$ax+by+cz=s+\bigl(a(x-1)+b(y-1)+c(z-1)\bigr).$$

So a positive integer \(n\) is representable with all three coefficients positive exactly when

$$n-s\in S.$$

This means the unreachable positive integers consist of two parts:

$$1,2,\dots,s-1,$$

and all integers of the form

$$s+g,$$

where \(g\) is a gap of \(S\), meaning \(g\notin S\).

If \(g(S)\) denotes the number of gaps of \(S\), and \(\sigma(S)\) denotes the sum of those gaps, then the required quantity is

$$U(p)=\frac{s(s-1)}{2}+s\,g(S)+\sigma(S).$$

Step 2: Use residues modulo the smallest generator

Because \(a=17^p\) is the smallest generator, working modulo \(a\) gives the smallest state space. For each residue \(r\in\{0,1,\dots,a-1\}\), define

$$w_r=\min\{m\in S:m\equiv r\pmod a\}.$$

The set \(\{w_0,\dots,w_{a-1}\}\) is the Apéry set of \(S\) with respect to \(a\). Once \(w_r\) is known, every reachable number in residue class \(r\) is

$$w_r,\ w_r+a,\ w_r+2a,\dots,$$

and every smaller nonnegative number in the same residue class is unreachable.

Step 3: Compute the Apéry set by shortest paths

Construct a directed graph on the residue classes modulo \(a\). From residue \(u\), add two edges

$$u\to u+b\pmod a,\qquad u\to u+c\pmod a.$$

The two edge weights are \(b\) and \(c\), respectively.

Any path from \(0\) to residue \(r\) represents a value \(jb+kc\) with that residue, and its path length is exactly that value. Therefore the shortest-path distance from \(0\) to \(r\) is the smallest semigroup element in that residue class.

No separate edge for \(+a\) is needed. Adding \(a\) does not change the residue and only increases the total value, so it can never improve a minimal representative. Hence Dijkstra's algorithm returns precisely the values \(w_r\).

Step 4: Recover the number of gaps

Write each Apéry element as

$$w_r=r+a q_r,\qquad q_r\in \mathbb{Z}_{\ge 0}.$$

Then the gaps in residue class \(r\) are

$$r,\ r+a,\ r+2a,\dots,r+(q_r-1)a,$$

so that residue class contributes exactly \(q_r\) gaps. Summing over all residue classes gives

$$g(S)=\sum_{r=0}^{a-1} q_r=\frac{1}{a}\sum_{r=0}^{a-1} w_r-\frac{a-1}{2}.$$

If we define

$$W_1=\sum_{r=0}^{a-1} w_r,$$

then

$$g(S)=\frac{W_1}{a}-\frac{a-1}{2}.$$

Step 5: Recover the sum of the gaps

The gaps in one residue class form an arithmetic progression, so their sum is

$$q_r r+\frac{a q_r(q_r-1)}{2}.$$

Substituting \(q_r=(w_r-r)/a\) and simplifying yields the standard Apéry identity

$$\sigma(S)=\frac{1}{2a}\sum_{r=0}^{a-1} w_r^2-\frac12\sum_{r=0}^{a-1} w_r+\frac{a^2-1}{12}.$$

With

$$W_2=\sum_{r=0}^{a-1} w_r^2,$$

this becomes

$$\sigma(S)=\frac{W_2}{2a}-\frac{W_1}{2}+\frac{a^2-1}{12}.$$

Step 6: Final formula

Combining the shift argument with the two Apéry identities gives

$$\boxed{U(p)=\frac{s(s-1)}{2}+s\left(\frac{W_1}{a}-\frac{a-1}{2}\right)+\left(\frac{W_2}{2a}-\frac{W_1}{2}+\frac{a^2-1}{12}\right).}$$

This is exactly the quantity evaluated modulo \(10^9+7\) by the implementations.

Worked Example: \(p=1\)

For \(p=1\), we have \(a=17\), \(b=19\), \(c=23\), and \(s=59\). The full Apéry set modulo \(17\) is

$$\left(w_0,\dots,w_{16}\right)=\left(0,69,19,88,38,107,23,92,42,111,61,130,46,115,65,134,84\right).$$

Therefore

$$W_1=1224,\qquad W_2=114036.$$

The number of gaps of \(S\) is

$$g(S)=\frac{1224}{17}-\frac{16}{2}=72-8=64,$$

and the sum of the gaps is

$$\sigma(S)=\frac{114036}{34}-\frac{1224}{2}+\frac{17^2-1}{12}=3354-612+24=2766.$$

Hence

$$U(1)=\frac{59\cdot 58}{2}+59\cdot 64+2766=1711+3776+2766=8253,$$

which matches the small checkpoint used by the implementations.

How the Code Works

The C++, Python, and Java implementations all follow the same pipeline. They first compute the three powers \(17^p\), \(19^p\), and \(23^p\), choose the smallest one as the modulus, and run Dijkstra's algorithm on the residue graph from Step 3.

The resulting distance table is the Apéry set. The implementation then accumulates the two moment sums \(W_1=\sum w_r\) and \(W_2=\sum w_r^2\), reducing everything modulo \(10^9+7\).

The formulas for \(g(S)\), \(\sigma(S)\), and \(U(p)\) contain division by \(2\), \(12\), and \(a\). Since the modulus is prime and coprime to those values, the code performs those divisions by modular inverses.

The same logic reproduces the checkpoints \(U(1)=8253\) and \(U(2)=60258000\) before evaluating the target case \(p=6\).

Complexity Analysis

The residue graph has \(a=17^p\) vertices and exactly two outgoing edges from each vertex. With a binary heap, Dijkstra's algorithm runs in \(O(a\log a)\) time and uses \(O(a)\) memory. The post-processing pass that forms \(W_1\) and \(W_2\) is linear, so the total complexity remains \(O(a\log a)\) time and \(O(a)\) space.

Footnotes and References

  1. Problem page: Project Euler 718
  2. Numerical semigroup: Wikipedia - Numerical semigroup
  3. Frobenius coin problem: Wikipedia - Frobenius coin problem
  4. Dijkstra's algorithm: Wikipedia - Dijkstra's algorithm
  5. Modular multiplicative inverse: Wikipedia - Modular multiplicative inverse

Problem 718 source code

C++

#include <cassert>
#include <cstdint>
#include <functional>
#include <iostream>
#include <limits>
#include <queue>
#include <utility>
#include <vector>

namespace {

using i128 = __int128_t;
using u64 = std::uint64_t;

constexpr u64 kMod = 1'000'000'007ULL;

u64 pow_u64(u64 base, int exp) {
    u64 result = 1;
    while (exp > 0) {
        if (exp & 1) {
            result *= base;
        }
        base *= base;
        exp >>= 1;
    }
    return result;
}

u64 mod_pow(u64 base, u64 exp) {
    u64 result = 1;
    base %= kMod;
    while (exp > 0) {
        if (exp & 1ULL) {
            result = static_cast<u64>((static_cast<i128>(result) * base) % kMod);
        }
        base = static_cast<u64>((static_cast<i128>(base) * base) % kMod);
        exp >>= 1ULL;
    }
    return result;
}

u64 G_mod(const int p) {
    const u64 A = pow_u64(17, p);
    const u64 B = pow_u64(19, p);
    const u64 C = pow_u64(23, p);

    const u64 inf = std::numeric_limits<u64>::max() / 4;
    std::vector<u64> dist(static_cast<std::size_t>(A), inf);

    using Node = std::pair<u64, int>;
    std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq;
    dist[0] = 0;
    pq.push({0, 0});

    while (!pq.empty()) {
        const auto [d, u] = pq.top();
        pq.pop();
        if (d != dist[static_cast<std::size_t>(u)]) {
            continue;
        }

        int v = static_cast<int>((static_cast<u64>(u) + B) % A);
        u64 nd = d + B;
        if (nd < dist[static_cast<std::size_t>(v)]) {
            dist[static_cast<std::size_t>(v)] = nd;
            pq.push({nd, v});
        }

        v = static_cast<int>((static_cast<u64>(u) + C) % A);
        nd = d + C;
        if (nd < dist[static_cast<std::size_t>(v)]) {
            dist[static_cast<std::size_t>(v)] = nd;
            pq.push({nd, v});
        }
    }

    u64 sum_w_mod = 0;
    u64 sum_w2_mod = 0;
    for (const u64 w : dist) {
        const u64 wm = w % kMod;
        sum_w_mod += wm;
        if (sum_w_mod >= kMod) {
            sum_w_mod -= kMod;
        }
        sum_w2_mod = (sum_w2_mod + static_cast<u64>((static_cast<i128>(wm) * wm) % kMod)) % kMod;
    }

    const u64 inv2 = (kMod + 1) / 2;
    const u64 inv12 = mod_pow(12, kMod - 2);
    const u64 Am = A % kMod;
    const u64 invA = mod_pow(Am, kMod - 2);

    u64 genus = static_cast<u64>((static_cast<i128>(sum_w_mod) * invA) % kMod);
    genus = (genus + kMod - static_cast<u64>((static_cast<i128>((Am + kMod - 1) % kMod) * inv2) % kMod)) % kMod;

    u64 gaps = static_cast<u64>((static_cast<i128>(sum_w2_mod) * inv2) % kMod);
    gaps = static_cast<u64>((static_cast<i128>(gaps) * invA) % kMod);
    gaps = (gaps + kMod - static_cast<u64>((static_cast<i128>(sum_w_mod) * inv2) % kMod)) % kMod;
    const u64 A2m = static_cast<u64>((static_cast<i128>(Am) * Am) % kMod);
    gaps = (gaps + static_cast<u64>((static_cast<i128>((A2m + kMod - 1) % kMod) * inv12) % kMod)) % kMod;

    const u64 shift = (A % kMod + B % kMod + C % kMod) % kMod;
    const u64 tri = static_cast<u64>((static_cast<i128>(shift) * ((shift + kMod - 1) % kMod) % kMod) * inv2 % kMod);

    u64 ans = tri;
    ans = (ans + static_cast<u64>((static_cast<i128>(genus) * shift) % kMod)) % kMod;
    ans = (ans + gaps) % kMod;
    return ans;
}

}  // namespace

int main() {
    assert(G_mod(1) == 8'253);
    assert(G_mod(2) == 60'258'000);

    std::cout << G_mod(6) << '\n';
    return 0;
}

Python

import heapq

def solve():
    MOD = 1000000007
    p = 6
    A = 17**p; B = 19**p; C = 23**p

    def mod_pow(base, exp):
        r = 1; base %= MOD
        while exp > 0:
            if exp & 1: r = r * base % MOD
            base = base * base % MOD; exp >>= 1
        return r

    INF = float('inf')
    dist = [INF] * A; dist[0] = 0
    pq = [(0, 0)]
    while pq:
        d, u = heapq.heappop(pq)
        if d != dist[u]: continue
        for step in [B, C]:
            v = (u + step) % A; nd = d + step
            if nd < dist[v]:
                dist[v] = nd; heapq.heappush(pq, (nd, v))

    sw = sw2 = 0
    for w in dist:
        wm = w % MOD
        sw = (sw + wm) % MOD
        sw2 = (sw2 + wm * wm) % MOD

    inv2 = (MOD + 1) // 2
    inv12 = mod_pow(12, MOD - 2)
    Am = A % MOD; invA = mod_pow(Am, MOD - 2)

    genus = sw * invA % MOD
    genus = (genus - (Am - 1) * inv2) % MOD

    gaps = sw2 * inv2 % MOD * invA % MOD
    gaps = (gaps - sw * inv2) % MOD
    A2m = Am * Am % MOD
    gaps = (gaps + (A2m - 1) * inv12) % MOD

    shift = (A % MOD + B % MOD + C % MOD) % MOD
    tri = shift * ((shift - 1) % MOD) % MOD * inv2 % MOD

    ans = (tri + genus * shift + gaps) % MOD
    return str(ans)

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

Java

import java.util.Arrays;
import java.util.PriorityQueue;

public class Euler718 {
    static final long kMod = 1000000007L;

    static long powU64(long base, int exp) {
        long result = 1;
        while (exp > 0) {
            if ((exp & 1) != 0) {
                result *= base;
            }
            base *= base;
            exp >>= 1;
        }
        return result;
    }

    static long modPow(long base, long exp) {
        long result = 1;
        base %= kMod;
        while (exp > 0) {
            if ((exp & 1) != 0) {
                result = (result * base) % kMod;
            }
            base = (base * base) % kMod;
            exp >>= 1;
        }
        return result;
    }

    static class Node implements Comparable<Node> {
        long d;
        int u;

        Node(long d, int u) {
            this.d = d;
            this.u = u;
        }

        @Override
        public int compareTo(Node other) {
            return Long.compare(this.d, other.d);
        }
    }

    public static String solve() {
        int p = 6;
        long A = powU64(17, p);
        long B = powU64(19, p);
        long C = powU64(23, p);

        long[] dist = new long[(int) A];
        Arrays.fill(dist, Long.MAX_VALUE / 4);

        PriorityQueue<Node> pq = new PriorityQueue<>();
        dist[0] = 0;
        pq.add(new Node(0, 0));

        while (!pq.isEmpty()) {
            Node node = pq.poll();
            long d = node.d;
            int u = node.u;

            if (d != dist[u]) {
                continue;
            }

            int v = (int) ((u + B) % A);
            long nd = d + B;
            if (nd < dist[v]) {
                dist[v] = nd;
                pq.add(new Node(nd, v));
            }

            v = (int) ((u + C) % A);
            nd = d + C;
            if (nd < dist[v]) {
                dist[v] = nd;
                pq.add(new Node(nd, v));
            }
        }

        long sumWMod = 0;
        long sumW2Mod = 0;
        for (long w : dist) {
            long wm = w % kMod;
            sumWMod = (sumWMod + wm) % kMod;
            sumW2Mod = (sumW2Mod + (wm * wm) % kMod) % kMod;
        }

        long inv2 = (kMod + 1) / 2;
        long inv12 = modPow(12, kMod - 2);
        long Am = A % kMod;
        long invA = modPow(Am, kMod - 2);

        long genus = (sumWMod * invA) % kMod;
        genus = (genus + kMod - (((Am + kMod - 1) % kMod * inv2) % kMod)) % kMod;

        long gaps = (sumW2Mod * inv2) % kMod;
        gaps = (gaps * invA) % kMod;
        gaps = (gaps + kMod - ((sumWMod * inv2) % kMod)) % kMod;
        long A2m = (Am * Am) % kMod;
        gaps = (gaps + (((A2m + kMod - 1) % kMod * inv12) % kMod)) % kMod;

        long shift = (A % kMod + B % kMod + C % kMod) % kMod;
        long tri = (shift * ((shift + kMod - 1) % kMod) % kMod * inv2) % kMod;

        long ans = tri;
        ans = (ans + (genus * shift) % kMod) % kMod;
        ans = (ans + gaps) % kMod;

        return Long.toString(ans);
    }

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