Problem 787: Bézout's Game

View on Project Euler

Project Euler Problem 787 Solution

EulerSolve provides an optimized solution for Project Euler Problem 787, Bézout's Game, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For a fixed \(n\), the game positions are the coprime ordered pairs \((a,b)\) of positive integers with \(a+b\le n\). A legal move goes to a smaller positive pair \((u,v)\) satisfying $$1\le u<a,\qquad 1\le v<b,\qquad |av-bu|=1.$$ The game convention used here treats every position with \(a=1\) or \(b=1\) as an immediate win. The goal is to count how many admissible positions are winning when \(n=10^9\). Mathematical Approach Let \(W(n)\) be the number of winning positions and \(T(n)\) the number of all primitive positions. The fast solution first counts every primitive pair, then subtracts the pairs that are losing. Step 1: Count All Primitive Positions Fix the sum \(s=a+b\). Then \(1\le a\le s-1\) and \(b=s-a\), so $$\gcd(a,b)=\gcd(a,s).$$ Therefore the number of coprime ordered pairs with \(a+b=s\) is exactly \(\varphi(s)\). Summing over every possible total gives $$T(n)=\sum_{s=2}^{n}\varphi(s)=\Phi(n)-1,$$ where $$\Phi(n)=\sum_{m\le n}\varphi(m).$$ The implementations evaluate \(\Phi(n)\) through the standard Möbius identity $$\Phi(n)=\frac{1+\sum_{d=1}^{n}\mu(d)\left\lfloor\frac{n}{d}\right\rfloor^2}{2},$$ which turns the problem into prefix queries for the Möbius function. Step 2: Characterize the Losing Positions Because the move rule is symmetric in \(a\) and \(b\), it is enough to understand the ordered half \(1<a<b\)....

Detailed mathematical approach

Problem Summary

For a fixed \(n\), the game positions are the coprime ordered pairs \((a,b)\) of positive integers with \(a+b\le n\). A legal move goes to a smaller positive pair \((u,v)\) satisfying

$$1\le u<a,\qquad 1\le v<b,\qquad |av-bu|=1.$$

The game convention used here treats every position with \(a=1\) or \(b=1\) as an immediate win. The goal is to count how many admissible positions are winning when \(n=10^9\).

Mathematical Approach

Let \(W(n)\) be the number of winning positions and \(T(n)\) the number of all primitive positions. The fast solution first counts every primitive pair, then subtracts the pairs that are losing.

Step 1: Count All Primitive Positions

Fix the sum \(s=a+b\). Then \(1\le a\le s-1\) and \(b=s-a\), so

$$\gcd(a,b)=\gcd(a,s).$$

Therefore the number of coprime ordered pairs with \(a+b=s\) is exactly \(\varphi(s)\). Summing over every possible total gives

$$T(n)=\sum_{s=2}^{n}\varphi(s)=\Phi(n)-1,$$

where

$$\Phi(n)=\sum_{m\le n}\varphi(m).$$

The implementations evaluate \(\Phi(n)\) through the standard Möbius identity

$$\Phi(n)=\frac{1+\sum_{d=1}^{n}\mu(d)\left\lfloor\frac{n}{d}\right\rfloor^2}{2},$$

which turns the problem into prefix queries for the Möbius function.

Step 2: Characterize the Losing Positions

Because the move rule is symmetric in \(a\) and \(b\), it is enough to understand the ordered half \(1<a<b\). Consider such a state and one of its legal children \((u,v)\). From

$$av-bu=\pm1$$

we obtain

$$v-u=\frac{(b-a)u\pm1}{a}\ge 0.$$

So every child also lies in the ordered half, except for the boundary case \((u,v)=(1,1)\), which is already a base win.

Now argue by induction on \(a+b\):

If \(a\) is even, then \(b\) is odd because \(\gcd(a,b)=1\). Reducing \(av-bu=\pm1\) modulo \(2\) gives \(u\equiv1\pmod 2\). Hence every child has odd smaller coordinate, so no child belongs to the losing family described below. Therefore the state is losing.

If \(a\) is odd and \(a>1\), the congruences \(bu\equiv \pm1\pmod a\) give two residues \(u\) and \(a-u\) in \(\{1,\dots,a-1\}\). Since \(a\) is odd, exactly one of them is even. Choosing that even residue produces a legal child with \(u<v\) and, by parity, \(v\) odd. Its smaller coordinate is even, so by induction that child is losing. Therefore the original state is winning.

Thus the losing positions in the ordered half are exactly the primitive pairs

$$ (a,b)=(2x,\,2y+1),\qquad 1\le x\le y,\qquad \gcd(2x,2y+1)=1. $$

Step 3: Count the Ordered Losing Shape Without the Coprime Condition

Define \(A(q)\) to be the number of pairs of the form \((2x,2y+1)\) with \(1\le x\le y\) and \(2x+2y+1\le q\), ignoring coprimality for the moment. Set

$$s=\left\lfloor\frac{q-1}{2}\right\rfloor.$$

Then the constraints become

$$1\le x\le y,\qquad x+y\le s.$$

For fixed \(x\), the variable \(y\) runs from \(x\) to \(s-x\), so

$$A(q)=\sum_{x=1}^{\lfloor s/2\rfloor}(s-2x+1).$$

This simplifies to the closed form

$$A(q)= \begin{cases} p^2, & s=2p,\\ p(p+1), & s=2p+1. \end{cases}$$

An equivalent version, closer to the implementation, is obtained from \(k=\left\lceil q/2\right\rceil\) and \(p=\left\lfloor k/2\right\rfloor\):

$$A(q)= \begin{cases} p^2, & k\text{ is odd},\\ p(p-1), & k\text{ is even}. \end{cases}$$

Step 4: Enforce Coprimality with Möbius Inversion Over Odd Divisors

Every pair counted by \(A(q)\) has one even coordinate and one odd coordinate, so any common divisor must itself be odd. That makes the Möbius inversion especially clean:

$$L_{1/2}(n)=\sum_{\substack{d\le n\\ d\text{ odd}}}\mu(d)\,A\!\left(\left\lfloor\frac{n}{d}\right\rfloor\right),$$

where \(L_{1/2}(n)\) is the number of losing positions in the ordered half \(a<b\).

To evaluate this quickly, define the ordinary Mertens function

$$M(x)=\sum_{m\le x}\mu(m)$$

and the odd-only prefix

$$M_{\mathrm{odd}}(x)=\sum_{\substack{d\le x\\ d\text{ odd}}}\mu(d).$$

The implementations use the identity

$$M_{\mathrm{odd}}(x)=\sum_{j\ge0} M\!\left(\left\lfloor\frac{x}{2^j}\right\rfloor\right),$$

which follows by expanding the right-hand side and observing that the contributions of even integers cancel in pairs, leaving only odd divisors.

Step 5: Use Symmetry and Assemble the Final Formula

Swapping coordinates preserves both the move rule and the winning/losing status. Since primitive pairs never lie on the diagonal except for \((1,1)\), which is already winning, the total number of losing positions is

$$L(n)=2L_{1/2}(n).$$

Therefore

$$\boxed{W(n)=\left(\sum_{s=2}^{n}\varphi(s)\right)-2\sum_{\substack{d\le n\\ d\text{ odd}}}\mu(d)\,A\!\left(\left\lfloor\frac{n}{d}\right\rfloor\right).}$$

This is exactly the arithmetic formula implemented by the fast solution.

Worked Example: \(n=20\)

First count all primitive states:

$$T(20)=\sum_{s=2}^{20}\varphi(s)=127.$$

Now count the ordered losing shape without enforcing coprimality. Here

$$A(20)=20,$$

coming from the twenty ordered pairs \((2x,2y+1)\) with \(2x<2y+1\) and \(2x+2y+1\le20\).

Among these, the only non-primitive family with odd gcd comes from \(d=3\), because

$$A\!\left(\left\lfloor\frac{20}{3}\right\rfloor\right)=A(6)=1,$$

while \(A(\lfloor20/d\rfloor)=0\) for every larger odd \(d\). Hence

$$L_{1/2}(20)=A(20)-A(6)=20-1=19,$$

$$L(20)=2\cdot19=38,$$

and finally

$$W(20)=127-38=89.$$

This matches direct recursive play on the small instance.

How the Code Works

The C++, Python, and Java implementations all follow the same numerical plan. They first build a presieved Möbius table up to a fixed bound and store its prefix sums. That gives instant access to \(M(x)\) for small \(x\).

For larger arguments, the implementation uses the classical divisor-block recurrence

$$M(x)=1-\sum_{l=2}^{x}(r-l+1)\,M\!\left(\left\lfloor\frac{x}{l}\right\rfloor\right),$$

where each block \([l,r]\) shares the same quotient \(\left\lfloor x/l\right\rfloor\). Memoization ensures that every large prefix value is computed only once.

The total number of primitive states is then obtained from the summatory-totient identity by grouping equal values of \(\left\lfloor n/d\right\rfloor\). The losing half is evaluated with the same quotient blocking, but using prefix differences of \(M_{\mathrm{odd}}\) and the explicit closed form for \(A(q)\).

After doubling the ordered-half losing count, the implementation subtracts it from the total primitive count. One version also checks the formula on small inputs with direct recursion before printing the large answer, but the actual \(n=10^9\) computation is purely arithmetic.

Complexity Analysis

Let \(B\) be the presieve limit. Building the Möbius sieve and its prefix sums costs \(O(B)\) time and \(O(B)\) memory. The outer summations for both the totient total and the losing count run over the distinct values of \(\left\lfloor n/d\right\rfloor\), which is \(O(\sqrt n)\).

The expensive part is answering large Mertens-prefix queries. Because those queries are memoized and each one is decomposed into quotient blocks as well, the practical running time stays far below linear in \(n\). Memory usage is dominated by the presieve arrays plus the cache of already-computed prefix values.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=787
  2. Euler's totient function: Wikipedia — Euler's totient function
  3. Möbius function: Wikipedia — Möbius function
  4. Mertens function: Wikipedia — Mertens function
  5. Möbius inversion formula: Wikipedia — Möbius inversion formula
  6. Farey sequence: Wikipedia — Farey sequence

Problem 787 source code

C++

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

using i64 = long long;
using i128 = __int128_t;

static std::string to_string_i128(i128 x) {
    if (x == 0) {
        return "0";
    }
    bool neg = x < 0;
    if (neg) {
        x = -x;
    }
    std::string s;
    while (x > 0) {
        const int d = static_cast<int>(x % 10);
        s.push_back(static_cast<char>('0' + d));
        x /= 10;
    }
    if (neg) {
        s.push_back('-');
    }
    std::reverse(s.begin(), s.end());
    return s;
}

struct MertensPrefix {
    int limit;
    std::vector<int> pref_mu;
    std::unordered_map<i64, i64> memo_mu;
    std::unordered_map<i64, i64> memo_odd_mu;

    explicit MertensPrefix(int lim) : limit(lim), pref_mu(lim + 1, 0) {
        std::vector<int> mu(lim + 1, 0);
        std::vector<int> primes;
        std::vector<bool> composite(lim + 1, false);

        mu[1] = 1;
        for (int i = 2; i <= lim; ++i) {
            if (!composite[i]) {
                primes.push_back(i);
                mu[i] = -1;
            }
            for (int p : primes) {
                const i64 v = 1LL * i * p;
                if (v > lim) {
                    break;
                }
                composite[static_cast<std::size_t>(v)] = true;
                if (i % p == 0) {
                    mu[static_cast<std::size_t>(v)] = 0;
                    break;
                }
                mu[static_cast<std::size_t>(v)] = -mu[i];
            }
        }

        for (int i = 1; i <= lim; ++i) {
            pref_mu[i] = pref_mu[i - 1] + mu[i];
        }
        memo_mu.reserve(1 << 16);
        memo_odd_mu.reserve(1 << 16);
    }

    i64 mu_prefix(i64 n) {
        if (n <= limit) {
            return pref_mu[static_cast<std::size_t>(n)];
        }
        const auto it = memo_mu.find(n);
        if (it != memo_mu.end()) {
            return it->second;
        }

        i64 ans = 1;
        i64 l = 2;
        while (l <= n) {
            const i64 q = n / l;
            const i64 r = n / q;
            ans -= (r - l + 1) * mu_prefix(q);
            l = r + 1;
        }

        memo_mu[n] = ans;
        return ans;
    }

    i64 odd_mu_prefix(i64 n) {
        if (n <= 0) {
            return 0;
        }
        const auto it = memo_odd_mu.find(n);
        if (it != memo_odd_mu.end()) {
            return it->second;
        }

        i64 ans = 0;
        i64 cur = n;
        while (cur > 0) {
            ans += mu_prefix(cur);
            cur >>= 1;
        }

        memo_odd_mu[n] = ans;
        return ans;
    }
};

static i128 sum_totients(i64 n, MertensPrefix& mp) {
    i128 s = 0;
    i64 l = 1;
    while (l <= n) {
        const i64 q = n / l;
        const i64 r = n / q;
        const i64 mu_seg = mp.mu_prefix(r) - mp.mu_prefix(l - 1);
        s += static_cast<i128>(mu_seg) * static_cast<i128>(q) * static_cast<i128>(q);
        l = r + 1;
    }
    return (s + 1) / 2;
}

static i64 f_odd_floor_sum(i64 m) {
    const i64 k = (m + 1) / 2;
    const i64 p = k / 2;
    if (k & 1LL) {
        return p * p;
    }
    return p * (p - 1);
}

static i128 solve_fast(i64 n) {
    const int sieve_limit = 2'000'000;
    MertensPrefix mp(sieve_limit);

    const i128 total = sum_totients(n, mp) - 1;

    i128 losing_half = 0;
    i64 l = 1;
    while (l <= n) {
        const i64 q = n / l;
        const i64 r = n / q;
        const i64 odd_mu_seg = mp.odd_mu_prefix(r) - mp.odd_mu_prefix(l - 1);
        losing_half += static_cast<i128>(odd_mu_seg) * static_cast<i128>(f_odd_floor_sum(q));
        l = r + 1;
    }

    const i128 losing = 2 * losing_half;
    return total - losing;
}

static bool has_losing_child(
    int a,
    int b,
    std::unordered_map<i64, bool>& memo,
    const std::function<bool(int, int)>& win
) {
    for (int u = 1; u < a; ++u) {
        const int m1 = b * u - 1;
        if (m1 % a == 0) {
            const int v = m1 / a;
            if (v > 0 && v < b) {
                if (!win(u, v)) {
                    return true;
                }
            }
        }

        const int m2 = b * u + 1;
        if (m2 % a == 0) {
            const int v = m2 / a;
            if (v > 0 && v < b) {
                if (!win(u, v)) {
                    return true;
                }
            }
        }
    }
    return false;
}

static i128 solve_bruteforce_game(int n) {
    std::unordered_map<i64, bool> memo;
    memo.reserve(1 << 16);

    std::function<bool(int, int)> win = [&](int a, int b) -> bool {
        const i64 key = (static_cast<i64>(a) << 32) | static_cast<unsigned int>(b);
        const auto it = memo.find(key);
        if (it != memo.end()) {
            return it->second;
        }

        if (a == 1 || b == 1) {
            memo[key] = true;
            return true;
        }

        const bool w = has_losing_child(a, b, memo, win);
        memo[key] = w;
        return w;
    };

    i128 ans = 0;
    for (int a = 1; a < n; ++a) {
        for (int b = 1; a + b <= n; ++b) {
            if (std::gcd(a, b) != 1) {
                continue;
            }
            if (win(a, b)) {
                ++ans;
            }
        }
    }
    return ans;
}

int main() {
    assert(solve_fast(4) == 5);
    assert(solve_fast(100) == 2043);
    assert(solve_fast(40) == solve_bruteforce_game(40));

    const i128 ans = solve_fast(1'000'000'000LL);
    std::cout << to_string_i128(ans) << '\n';
    return 0;
}

Python

class MertensPrefix:
    def __init__(self, lim):
        self.limit = lim
        self.pref_mu = [0] * (lim + 1)
        
        mu = [0] * (lim + 1)
        primes = []
        composite = [False] * (lim + 1)
        
        mu[1] = 1
        for i in range(2, lim + 1):
            if not composite[i]:
                primes.append(i)
                mu[i] = -1
            for p in primes:
                v = i * p
                if v > lim:
                    break
                composite[v] = True
                if i % p == 0:
                    mu[v] = 0
                    break
                mu[v] = -mu[i]
                
        for i in range(1, lim + 1):
            self.pref_mu[i] = self.pref_mu[i - 1] + mu[i]
            
        self.memo_mu = {}
        self.memo_odd_mu = {}

    def mu_prefix(self, n):
        if n <= self.limit:
            return self.pref_mu[n]
        if n in self.memo_mu:
            return self.memo_mu[n]

        ans = 1
        l = 2
        while l <= n:
            q = n // l
            r = n // q
            ans -= (r - l + 1) * self.mu_prefix(q)
            l = r + 1

        self.memo_mu[n] = ans
        return ans

    def odd_mu_prefix(self, n):
        if n <= 0:
            return 0
        if n in self.memo_odd_mu:
            return self.memo_odd_mu[n]

        ans = 0
        cur = n
        while cur > 0:
            ans += self.mu_prefix(cur)
            cur >>= 1

        self.memo_odd_mu[n] = ans
        return ans

def sum_totients(n, mp):
    s = 0
    l = 1
    while l <= n:
        q = n // l
        r = n // q
        mu_seg = mp.mu_prefix(r) - mp.mu_prefix(l - 1)
        s += mu_seg * q * q
        l = r + 1
    return (s + 1) // 2

def f_odd_floor_sum(m):
    k = (m + 1) // 2
    p = k // 2
    if k % 2 == 1:
        return p * p
    return p * (p - 1)

def solve_fast(n):
    sieve_limit = 2000000
    mp = MertensPrefix(sieve_limit)

    total = sum_totients(n, mp) - 1

    losing_half = 0
    l = 1
    while l <= n:
        q = n // l
        r = n // q
        odd_mu_seg = mp.odd_mu_prefix(r) - mp.odd_mu_prefix(l - 1)
        losing_half += odd_mu_seg * f_odd_floor_sum(q)
        l = r + 1

    losing = 2 * losing_half
    return total - losing

def solve():
    return str(solve_fast(10**9))

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

Java

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

public class Euler787 {

    static class MertensPrefix {
        int limit;
        int[] prefMu;
        HashMap<Long, Long> memoMu;
        HashMap<Long, Long> memoOddMu;

        MertensPrefix(int lim) {
            limit = lim;
            prefMu = new int[lim + 1];

            int[] mu = new int[lim + 1];
            ArrayList<Integer> primes = new ArrayList<>();
            boolean[] composite = new boolean[lim + 1];

            mu[1] = 1;
            for (int i = 2; i <= lim; ++i) {
                if (!composite[i]) {
                    primes.add(i);
                    mu[i] = -1;
                }
                for (int p : primes) {
                    long v = (long) i * p;
                    if (v > lim)
                        break;
                    composite[(int) v] = true;
                    if (i % p == 0) {
                        mu[(int) v] = 0;
                        break;
                    }
                    mu[(int) v] = -mu[i];
                }
            }

            for (int i = 1; i <= lim; ++i) {
                prefMu[i] = prefMu[i - 1] + mu[i];
            }

            memoMu = new HashMap<>(); // Primitives mappings would be faster but this requires less external libraries
            memoOddMu = new HashMap<>();
        }

        long muPrefix(long n) {
            if (n <= limit) {
                return prefMu[(int) n];
            }
            Long cached = memoMu.get(n);
            if (cached != null) {
                return cached;
            }

            long ans = 1;
            long l = 2;
            while (l <= n) {
                long q = n / l;
                long r = n / q;
                ans -= (r - l + 1) * muPrefix(q);
                l = r + 1;
            }

            memoMu.put(n, ans);
            return ans;
        }

        long oddMuPrefix(long n) {
            if (n <= 0)
                return 0;
            Long cached = memoOddMu.get(n);
            if (cached != null)
                return cached;

            long ans = 0;
            long cur = n;
            while (cur > 0) {
                ans += muPrefix(cur);
                cur >>= 1;
            }

            memoOddMu.put(n, ans);
            return ans;
        }
    }

    static long sumTotients(long n, MertensPrefix mp) {
        long s = 0;
        long l = 1;
        while (l <= n) {
            long q = n / l;
            long r = n / q;
            long muSeg = mp.muPrefix(r) - mp.muPrefix(l - 1);
            s += muSeg * q * q;
            l = r + 1;
        }
        return (s + 1) / 2;
    }

    static long fOddFloorSum(long m) {
        long k = (m + 1) / 2;
        long p = k / 2;
        if ((k & 1) == 1) {
            return p * p;
        }
        return p * (p - 1);
    }

    static long solveFast(long n) {
        int sieveLimit = 2000000;
        MertensPrefix mp = new MertensPrefix(sieveLimit);

        long total = sumTotients(n, mp) - 1;

        long losingHalf = 0;
        long l = 1;
        while (l <= n) {
            long q = n / l;
            long r = n / q;
            long oddMuSeg = mp.oddMuPrefix(r) - mp.oddMuPrefix(l - 1);
            losingHalf += oddMuSeg * fOddFloorSum(q);
            l = r + 1;
        }

        long losing = 2 * losingHalf;
        return total - losing;
    }

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

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