Project Euler Problem 665

Two players play a game with two piles of stones, alternating turns.

Project Euler Problem 665

Solution

Answer: 11541685709674

Let a losing position (a P-position) be $(a,b)$ with $a\le b$.

As in Wythoff Nim, the key observation is that every positive integer appears in exactly one losing position (once across all coordinates). This follows because:

  • removing stones from a single pile means two P-positions cannot share a coordinate;
  • every non-losing position must move to a unique earlier P-position.

For a P-position $(a,b)$, every reachable position is winning. Thus the following five infinite families are forbidden for future P-positions:

$$(a,b+t),\qquad (a+t,b),\qquad (a+t,b+t),$$

$$(a+2t,b+t),\qquad (a+t,b+2t), \qquad t\ge1.$$

Equivalently, a candidate $(n,m)$ is winning iff it lies on one of five “attack lines” generated by an earlier P-position. A position is losing iff it lies on none of them.

The efficient trick is to encode each family by an intercept:

  • slope $1$: $m-n = c$,
  • slope $2$: $m-2n = c$,
  • slope $1/2$: $2m-n = c$,

and maintain hash sets of active intercepts. Then each candidate $(n,m)$ can be tested in O(1) time by checking whether any corresponding intercept is already present. This yields an essentially linear-time greedy construction of all P-positions up to $n+m\le M$. This observation is known among successful solutions to the problem.

The greedy construction is:

  1. Take the smallest unused integer $n$.
  2. Find the smallest unused $m\ge n$ such that $(n,m)$ is not attacked by any previous P-position.
  3. Record $(n,m)$, mark $n,m$ used, and add $n+m$ to the sum if $n+m\le M$.

A clean Python implementation:

def f(M):
    used = bytearray(M + 10)

    # attack-line intercept sets
    d1 = set()   # m - n
    d2 = set()   # m - 2n
    d3 = set()   # 2m - n

    ans = 0
    m_ptr = 1

    for n in range(1, M + 1):
        if used[n]:
            continue

        m = max(m_ptr, n)

        while True:
            if not used[m]:
                # check whether (n,m) is attacked
                attacked = (
                    (m - n) in d1 or
                    (m - 2 * n) in d2 or
                    (2 * m - n) in d3
                )

                if not attacked:
                    break
            m += 1

        used[n] = 1
        used[m] = 1

        if n + m <= M:
            ans += n + m

        # add attack lines generated by this P-position
        d1.add(m - n)
        d2.add(m - 2 * n)
        d3.add(2 * m - n)

        m_ptr = m

    return ans

print(f(10))     # 21
print(f(100))    # 1164
print(f(1000))   # 117002
print(f(10**7))

The implementation reproduces the given checks:

  • $f(10)=21$
  • $f(100)=1164$
  • $f(1000)=117002$

and for $M=10^7$ computes: 11541685709674.

Answer: 11541685709674