Problem 784: Reciprocal Pairs

View on Project Euler

Project Euler Problem 784 Solution

EulerSolve provides an optimized solution for Project Euler Problem 784, Reciprocal Pairs, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For a given bound \(n\), we sum \(p+q\) over all triples of positive integers \((p,q,r)\) satisfying $$q > p > r,\qquad pr \equiv 1 \pmod q,\qquad qr \equiv 1 \pmod p,\qquad p \le n.$$ A naive search over all triples is far too expensive. The key observation is that, once \(r\) is fixed, every valid pair \((p,q)\) comes from a divisor of \(r^2-1\), so the problem becomes a structured divisor-enumeration task instead of a three-dimensional brute-force scan. Mathematical Approach Let \(S(n)\) denote the required total: $$S(n)=\sum (p+q),$$ where the sum ranges over all triples \((p,q,r)\) satisfying the conditions above. The derivation below explains why the implementations only need to loop over \(r\) and divisors of \(r^2-1\). Step 1: Turn the congruences into exact equations Because \(pr \equiv 1 \pmod q\), there is an integer \(a\) such that $$pr-1=aq.$$ Likewise, from \(qr \equiv 1 \pmod p\), there is an integer \(b\) with $$qr-1=bp.$$ Since \(q>r\), we have $$0 \le a=\frac{pr-1}{q} < p.$$ Reduce the first equation modulo \(p\). Because \(qr \equiv 1 \pmod p\), the number \(q\) is the inverse of \(r\) modulo \(p\)....

Detailed mathematical approach

Problem Summary

For a given bound \(n\), we sum \(p+q\) over all triples of positive integers \((p,q,r)\) satisfying

$$q > p > r,\qquad pr \equiv 1 \pmod q,\qquad qr \equiv 1 \pmod p,\qquad p \le n.$$

A naive search over all triples is far too expensive. The key observation is that, once \(r\) is fixed, every valid pair \((p,q)\) comes from a divisor of \(r^2-1\), so the problem becomes a structured divisor-enumeration task instead of a three-dimensional brute-force scan.

Mathematical Approach

Let \(S(n)\) denote the required total:

$$S(n)=\sum (p+q),$$

where the sum ranges over all triples \((p,q,r)\) satisfying the conditions above. The derivation below explains why the implementations only need to loop over \(r\) and divisors of \(r^2-1\).

Step 1: Turn the congruences into exact equations

Because \(pr \equiv 1 \pmod q\), there is an integer \(a\) such that

$$pr-1=aq.$$

Likewise, from \(qr \equiv 1 \pmod p\), there is an integer \(b\) with

$$qr-1=bp.$$

Since \(q>r\), we have

$$0 \le a=\frac{pr-1}{q} < p.$$

Reduce the first equation modulo \(p\). Because \(qr \equiv 1 \pmod p\), the number \(q\) is the inverse of \(r\) modulo \(p\). Multiplying \(pr-1=aq\) by \(r\) modulo \(p\) gives

$$-r \equiv aqr \equiv a \pmod p.$$

The unique integer in \([0,p-1]\) congruent to \(-r\) modulo \(p\) is \(p-r\), so

$$a=p-r.$$

Therefore

$$pr-1=(p-r)q,\qquad q=\frac{pr-1}{p-r}.$$

By symmetry, the second congruence yields

$$qr-1=(q-r)p,\qquad p=\frac{qr-1}{q-r}.$$

Step 2: Measure how far \(p\) and \(q\) are from \(r\)

Set

$$d=p-r,\qquad e=q-r,$$

so that

$$p=r+d,\qquad q=r+e,\qquad d>0,\ e>0.$$

Insert \(p=r+d\) into the formula for \(q\):

$$q=\frac{(r+d)r-1}{d}=r+\frac{r^2-1}{d}.$$

Hence

$$e=\frac{r^2-1}{d},\qquad de=r^2-1.$$

This is the core reduction. For a fixed \(r\), every valid triple corresponds to a factor pair of \(r^2-1\).

Step 3: Determine the exact divisor range

The condition \(p \le n\) becomes

$$r+d \le n,\qquad d \le n-r.$$

The ordering \(q>p\) is equivalent to \(e>d\). Since \(de=r^2-1\), this means

$$d^2 < r^2-1.$$

For integers \(r \ge 2\), that is equivalent to

$$d \le r-1.$$

So the admissible divisors are exactly those satisfying

$$1 \le d \le m_r,\qquad m_r=\min(r-1,n-r),\qquad d \mid (r^2-1).$$

Once such a divisor is chosen, the corresponding pair is forced:

$$p=r+d,\qquad q=r+\frac{r^2-1}{d}.$$

Step 4: Prove the converse direction

The reduction is not just necessary; it is exact. Suppose \(r \ge 2\) and \(d\) is a divisor of \(r^2-1\) with \(1 \le d \le m_r\). Define

$$e=\frac{r^2-1}{d},\qquad p=r+d,\qquad q=r+e.$$

Then

$$pr-1=r(r+d)-1=r^2+rd-1=d(r+e)=dq=(p-r)q,$$

so \(pr \equiv 1 \pmod q\). Similarly,

$$qr-1=r(r+e)-1=r^2+re-1=e(r+d)=ep=(q-r)p,$$

so \(qr \equiv 1 \pmod p\). The bound \(d \le n-r\) guarantees \(p \le n\), and \(d \le r-1\) guarantees \(q>p\). Therefore every admissible divisor produces one valid triple, and every valid triple produces one admissible divisor. This is a bijection.

Step 5: Final summation formula

For a fixed \(r\), each valid divisor contributes

$$p+q=(r+d)+\left(r+\frac{r^2-1}{d}\right)=2r+d+\frac{r^2-1}{d}.$$

Thus

$$S(n)=\sum_{r=2}^{n-1}\ \sum_{d \mid (r^2-1),\ 1 \le d \le m_r}\left(2r+d+\frac{r^2-1}{d}\right),$$

where \(m_r=\min(r-1,n-r)\). This is exactly the quantity computed by the implementations.

Worked Example: \(n=5\)

Only \(r=2,3,4\) can contribute.

For \(r=2\), we have \(r^2-1=3\) and \(m_r=\min(1,3)=1\). The only admissible divisor is \(d=1\), giving

$$p=2+1=3,\qquad q=2+\frac{3}{1}=5,$$

so the contribution is \(3+5=8\).

For \(r=3\), we have \(r^2-1=8\) and \(m_r=\min(2,2)=2\). The admissible divisors are \(d=1,2\):

$$d=1 \Rightarrow (p,q)=(4,11),\quad p+q=15,$$

$$d=2 \Rightarrow (p,q)=(5,7),\quad p+q=12.$$

For \(r=4\), we have \(r^2-1=15\) and \(m_r=\min(3,1)=1\). Only \(d=1\) is allowed, producing

$$ (p,q)=(5,19),\quad p+q=24. $$

Adding everything gives

$$S(5)=8+15+12+24=59.$$

How the Code Works

The C++, Python, and Java implementations all follow the same structure. They begin by building a smallest-prime-factor table up to slightly above \(n\). That allows fast factorizations of \(r-1\) and \(r+1\) for every \(r\).

For each \(r\) from \(2\) to \(n-1\), the implementation computes \(m_r=\min(r-1,n-r)\). If \(m_r \le 0\), that \(r\) cannot contribute. Otherwise, it factors \(r-1\) and \(r+1\), merges the prime exponents, and thereby obtains the prime factorization of

$$r^2-1=(r-1)(r+1).$$

Next, it recursively enumerates divisors from that factorization. During this search, branches are pruned as soon as the partial divisor already exceeds \(m_r\), because such a divisor cannot yield a valid \(p\).

Whenever a complete admissible divisor \(d\) is produced, the implementation computes

$$p=r+d,\qquad q=r+\frac{r^2-1}{d},$$

and adds \(p+q\) to the running total. The whole algorithm is therefore a careful divisor-generation procedure driven by the exact bijection proved above.

Complexity Analysis

Building the smallest-prime-factor table costs \(O(n\log\log n)\) time and \(O(n)\) memory. For each \(r\), factoring \(r-1\) and \(r+1\) is fast because the factorization follows the precomputed table. The dominant work is enumerating admissible divisors of \(r^2-1\), so the total running time is well described by

$$O\!\left(n\log\log n+\sum_{r=2}^{n-1}\tau(r^2-1)\right),$$

up to the usual small-factor savings from pruning divisors larger than \(m_r\). In practice, this is dramatically smaller than brute-force enumeration over all candidate triples.

Footnotes and References

  1. Project Euler problem page: https://projecteuler.net/problem=784
  2. Modular arithmetic: Wikipedia — Modular arithmetic
  3. Divisor function: Wikipedia — Divisor function
  4. Fundamental theorem of arithmetic: Wikipedia — Fundamental theorem of arithmetic
  5. Sieve of Eratosthenes: Wikipedia — Sieve of Eratosthenes

Problem 784 source code

C++

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

using u64 = std::uint64_t;
using u128 = unsigned __int128;

static std::vector<int> build_spf(int n) {
    std::vector<int> spf(n + 1);
    for (int i = 0; i <= n; ++i) {
        spf[i] = i;
    }
    for (int i = 2; 1LL * i * i <= n; ++i) {
        if (spf[i] != i) {
            continue;
        }
        for (int j = i * i; j <= n; j += i) {
            if (spf[j] == j) {
                spf[j] = i;
            }
        }
    }
    return spf;
}

static std::vector<std::pair<int, int>> factorize_small(int x, const std::vector<int>& spf) {
    std::vector<std::pair<int, int>> out;
    while (x > 1) {
        const int p = spf[x];
        int c = 0;
        while (x % p == 0) {
            x /= p;
            ++c;
        }
        out.push_back({p, c});
    }
    return out;
}

static std::vector<std::pair<int, int>> merge_factorizations(
    const std::vector<std::pair<int, int>>& a,
    const std::vector<std::pair<int, int>>& b
) {
    std::vector<std::pair<int, int>> f = a;
    for (const auto& [p, c] : b) {
        bool found = false;
        for (auto& [q, d] : f) {
            if (q == p) {
                d += c;
                found = true;
                break;
            }
        }
        if (!found) {
            f.push_back({p, c});
        }
    }
    return f;
}

static std::string to_string_u128(u128 x) {
    if (x == 0) {
        return "0";
    }
    std::string s;
    while (x > 0) {
        const int digit = static_cast<int>(x % 10);
        s.push_back(static_cast<char>('0' + digit));
        x /= 10;
    }
    std::reverse(s.begin(), s.end());
    return s;
}

static u128 solve_fast(int n) {
    const auto spf = build_spf(n + 1);
    u128 ans = 0;

    for (int r = 2; r < n; ++r) {
        const int m = std::min(r - 1, n - r);
        if (m <= 0) {
            continue;
        }

        const auto fa = factorize_small(r - 1, spf);
        const auto fb = factorize_small(r + 1, spf);
        auto factors = merge_factorizations(fa, fb);

        const u64 sq_minus_1 = static_cast<u64>(r) * static_cast<u64>(r) - 1ULL;

        std::function<void(int, u64)> dfs = [&](int idx, u64 cur) {
            if (idx == static_cast<int>(factors.size())) {
                if (cur == 0 || cur > static_cast<u64>(m)) {
                    return;
                }
                const u64 d = cur;
                const u64 p = static_cast<u64>(r) + d;
                const u64 q = static_cast<u64>(r) + sq_minus_1 / d;
                ans += static_cast<u128>(p) + static_cast<u128>(q);
                return;
            }

            const auto [prime, exp] = factors[idx];
            u64 val = cur;
            for (int e = 0; e <= exp; ++e) {
                if (val > static_cast<u64>(m)) {
                    break;
                }
                dfs(idx + 1, val);
                if (e == exp || val > static_cast<u64>(m) / static_cast<u64>(prime)) {
                    break;
                }
                val *= static_cast<u64>(prime);
            }
        };

        dfs(0, 1);
    }

    return ans;
}

static u128 solve_bruteforce(int n) {
    u128 ans = 0;
    for (int p = 2; p <= n; ++p) {
        for (int r = 1; r < p; ++r) {
            const int den = p - r;
            const int num = p * r - 1;
            if (num % den != 0) {
                continue;
            }
            const int q = num / den;
            if (q <= p) {
                continue;
            }
            if ((static_cast<long long>(p) * r) % q != 1) {
                continue;
            }
            if ((static_cast<long long>(q) * r) % p != 1) {
                continue;
            }
            ans += static_cast<u128>(p + q);
        }
    }
    return ans;
}

int main() {
    assert(to_string_u128(solve_fast(5)) == "59");
    assert(to_string_u128(solve_fast(100)) == "697317");
    assert(solve_fast(250) == solve_bruteforce(250));

    const u128 ans = solve_fast(2'000'000);
    std::cout << to_string_u128(ans) << '\n';
    return 0;
}

Python

def solve():
    n = 2000000
    spf = list(range(n+2))
    for i in range(2, int((n+1)**0.5)+1):
        if spf[i] == i:
            for j in range(i*i, n+2, i):
                if spf[j] == j: spf[j] = i

    def factorize(x):
        fac = []
        while x > 1:
            p = spf[x]; c = 0
            while x % p == 0: x //= p; c += 1
            fac.append((p, c))
        return fac

    ans = 0
    for r in range(2, n):
        m = min(r-1, n-r)
        if m <= 0: continue
        fa = factorize(r-1); fb = factorize(r+1)
        fac = dict(fa)
        for p, c in fb: fac[p] = fac.get(p, 0) + c
        factors = list(fac.items())
        sq1 = r*r - 1
        def dfs(idx, cur):
            nonlocal ans
            if idx == len(factors):
                if cur == 0 or cur > m: return
                d = cur; p_ = r + d; q_ = r + sq1 // d
                ans += p_ + q_; return
            pr, exp = factors[idx]; val = cur
            for e in range(exp+1):
                if val > m: break
                dfs(idx+1, val)
                if e == exp or val > m // pr: break
                val *= pr
        dfs(0, 1)

    return str(ans)

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

Java

import java.util.ArrayList;
import java.util.HashMap;

public class Euler784 {

    static int[] buildSpf(int n) {
        int[] spf = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            spf[i] = i;
        }
        for (int i = 2; i * i <= n; i++) {
            if (spf[i] == i) {
                for (int j = i * i; j <= n; j += i) {
                    if (spf[j] == j) {
                        spf[j] = i;
                    }
                }
            }
        }
        return spf;
    }

    static class Factor {
        int p, c;

        Factor(int p, int c) {
            this.p = p;
            this.c = c;
        }
    }

    static ArrayList<Factor> factorizeSmall(int x, int[] spf) {
        ArrayList<Factor> factors = new ArrayList<>();
        while (x > 1) {
            int p = spf[x];
            int c = 0;
            while (x % p == 0) {
                x /= p;
                c++;
            }
            factors.add(new Factor(p, c));
        }
        return factors;
    }

    static ArrayList<Factor> mergeFactorizations(ArrayList<Factor> a, ArrayList<Factor> b) {
        HashMap<Integer, Integer> merged = new HashMap<>();
        for (Factor f : a) {
            merged.put(f.p, merged.getOrDefault(f.p, 0) + f.c);
        }
        for (Factor f : b) {
            merged.put(f.p, merged.getOrDefault(f.p, 0) + f.c);
        }
        ArrayList<Factor> res = new ArrayList<>();
        for (HashMap.Entry<Integer, Integer> entry : merged.entrySet()) {
            res.add(new Factor(entry.getKey(), entry.getValue()));
        }
        return res;
    }

    static long solveFast(int n) {
        int[] spf = buildSpf(n + 1);
        long ans = 0;

        for (int r = 2; r < n; r++) {
            int m = Math.min(r - 1, n - r);
            if (m <= 0)
                continue;

            ArrayList<Factor> fa = factorizeSmall(r - 1, spf);
            ArrayList<Factor> fb = factorizeSmall(r + 1, spf);
            ArrayList<Factor> factors = mergeFactorizations(fa, fb);

            long sqMinus1 = (long) r * r - 1L;
            ans += dfs(0, 1L, factors, m, r, sqMinus1);
        }

        return ans;
    }

    static long dfs(int idx, long cur, ArrayList<Factor> factors, int m, long r, long sqMinus1) {
        if (idx == factors.size()) {
            if (cur == 0 || cur > m)
                return 0;
            long p = r + cur;
            long q = r + sqMinus1 / cur;
            return p + q;
        }

        Factor f = factors.get(idx);
        long val = cur;
        long sum = 0;

        for (int e = 0; e <= f.c; e++) {
            if (val > m)
                break;
            sum += dfs(idx + 1, val, factors, m, r, sqMinus1);
            if (e == f.c || val > m / f.p)
                break;
            val *= f.p;
        }

        return sum;
    }

    public static String solve() {
        return Long.toString(solveFast(2000000));
    }

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