Problem 310: Nim Square

Alice and Bob play the game Nim Square.
Nim Square is just like ordinary three-heap normal play Nim, but the players may only remove a square number of stones from a heap.
The number of stones in the three heaps is represented by the ordered triple (a,b,c).
If 0 <= a <= b <= c <= 29 then the number of losing positions for the next player is 1160.

Find the number of losing positions for the next player if 0 <= a <= b <= c <= 100 000.

Anyway, writing a quick'n'dirty bruteForce solver is good idea because it takes just a few minutes and helps verifying that the "smart" solver is correct (for small values).
See the bruteForce function and #define BRUTE_FORCE.

The basic idea is to look at a single heap.
The mex-value of an empty heap is zero because then the game is lost.
Whenever I want to determine the mex-value of a heap with size elements then I analyze all heaps size - i*i.
For example, to compute the mex-value of 13 I need the mex-values of mex(13 - 1^2) = mex(12), mex(13 - 2^2) = mex(9) and mex(13 - 3^2) = mex(4).
The mex-value of 13 is the smallest number >= 0 which is not among these values.

Maybe it becomes a bit clearer when looking at the mex-values between 0 and 13:

heap size012345678910111213mex01012010120101

Since mex(12) = 0, mex(9) = 2 and mex(4) = 2 the set of its "children mex-values" is \{ 0, 2 \}.
The smallest number >= 0 which is not part of that set is 1.

The first part of search implements this straight-forward algorithm.
Looking at my logging output, the maximum mex-value for heaps smaller than 100000 is surprisingly small: just 74.
A simple std::bitset instead of std::set makes this process pretty fast.

However, the original problem was about three heaps - and not a single heap.
I could write three nested loops and check whether mex(a) ^ mex(b) ^ mex(c) is zero (= lost, same XOR idea like in problem 301).
But that's not very efficient: it would require approx 100000^3 iterations (actually a bit less, but still a huge number).

The following observations speed up the process (\oplus is the XOR-operation):(1)x \oplus y is only zero if x = y(2)x \oplus y \oplus z = x \oplus (y \oplus z)

To count the number of lost positions in a three heap Nim Square game:

while analyzing triples (a,b,c) many pairs (b,c) are repeatedly encountered

especially if a iterates backwards, that means from limit to 0, then all pairs (b,c) of the previous iteration will appears again

of course a few new pairs (b,c) will appear, too, where b = a and b <= c

I count how often these pairs (b,c) produce mex(b) ^ mex(c) and store it in my frequency container such that frequency[mex[b] ^ mex[c]]++

for each a the number in frequency[mex[a]] is the number of lost games because of equation (1)

Alternative Approaches

Clever use of combinatorics can eliminate the last step. I opted for the "programming" solution instead of the hairy "mathematical approach".

Interactive test

You can submit your own input to my program and it will be instantly processed at my server:

Input data (separated by spaces or newlines):

This is equivalent toecho 29 | ./310

Output:

(please click 'Go !')

Note: the original problem's input 100000cannot be enteredbecause just copying results is a soft skill reserved for idiots.

(this interactive test is still under development, computations will be aborted after one second)

My code

… was written in C++11 and can be compiled with G++, Clang++, Visual C++. You can download it, too.

#include<iostream>

#include<vector>

#include<bitset>

#include<unordered_set>

#include<algorithm>

// needed to compute a unique ID for each position (bruteForce only)

constunsignedlonglong MaxValue = 100000;

// determine whether a single position is won (true) or lost (false)

// reasonably fast for small values but eats quite a good amount of memory (about 800 MByte to fully analyze a,b,c <= 500)

boolbruteForce(unsignedint a, unsignedint b, unsignedint c)

{

// game over ?

if (a == 0 && b == 0 && c == 0)

returnfalse;

// sort them in ascending order: a <= b <= c

if (a > b)

std::swap(a, b);

if (b > c)

std::swap(b, c);

if (a > b)

std::swap(a, b);

// memoize

auto id = a * MaxValue * MaxValue + b * MaxValue + c;

// two separate caches

staticstd::unordered_set<unsignedlonglong> cacheWon;

if (cacheWon .count(id) != 0)

returntrue;

staticstd::unordered_set<unsignedlonglong> cacheLost;

if (cacheLost.count(id) != 0)

returnfalse;

// try every possible move:

// take a square number of stones

for (unsignedint i = 1; i*i <= c; i++)

{

// take a square number of stones from the smallest stack

if (i*i <= a && !bruteForce(a - i*i, b, c))

{

cacheWon.insert(id);

returntrue;

}

// take a square number of stones from the stack in the middle

if (i*i <= b && !bruteForce(a, b - i*i, c))

{

cacheWon.insert(id);

returntrue;

}

// take a square number of stones from the largest stack

if (!bruteForce(a, b, c - i*i))

{

cacheWon.insert(id);

returntrue;

}

}

// no winning move ... meaning that the current player loses

cacheLost.insert(id);

returnfalse;

}

// count all lost positions, just under 5 seconds for limit = 100000

unsignedlonglongsearch(unsignedint limit)

{

// based on this great posting: https://mathstrek.blog/2012/08/05/combinatorial-game-theory-iii/

// compute Nim values for a single pile

std::vector<unsignedint>mex(limit + 1, 0); // fill with zeros

for (unsignedint size = 0; size <= limit; size++)

{

// I saw in my experiments that Nim values are pretty small for limit <= 100000 (<= 74)

constsize_t NimLimit = 80;

// collect all Nim values after each possible move

std::bitset<NimLimit> moves; // initialized with zeros

for (unsignedint i = 1; i*i <= size; i++)

// take i^2 stones from the heap

moves[mex[size - i*i]] = true;

// find the smallest non-negative number which is NOT part of the "moves" container

Those links are just an unordered selection of source code I found with a semi-automatic search script on Google/Bing/GitHub/whatever.
You will probably stumble upon better solutions when searching on your own.
Maybe not all linked resources produce the correct result and/or exceed time/memory limits.

Heatmap

Please click on a problem's number to open my solution to that problem:

green

solutions solve the original Project Euler problem and have a perfect score of 100% at Hackerrank, too

yellow

solutions score less than 100% at Hackerrank (but still solve the original problem easily)

gray

problems are already solved but I haven't published my solution yet

blue

solutions are relevant for Project Euler only: there wasn't a Hackerrank version of it (at the time I solved it) or it differed too much

orange

problems are solved but exceed the time limit of one minute or the memory limit of 256 MByte

red

problems are not solved yet but I wrote a simulation to approximate the result or verified at least the given example - usually I sketched a few ideas, too

black

problems are solved but access to the solution is blocked for a few days until the next problem is published

[new]

the flashing problem is the one I solved most recently

I stopped working on Project Euler problems around the time they released 617.

The 310 solved problems (that's level 12) had an average difficulty of 32.6&percnt; at Project Euler and
I scored 13526 points (out of 15700 possible points, top rank was 17 out of &approx;60000 in August 2017)
at Hackerrank's Project Euler+.

My username at Project Euler is stephanbrumme while it's stbrumme at Hackerrank.

Copyright

I hope you enjoy my code and learn something - or give me feedback how I can improve my solutions.All of my solutions can be used for any purpose and I am in no way liable for any damages caused.You can even remove my name and claim it's yours. But then you shall burn in hell.

The problems and most of the problems' images were created by Project Euler.Thanks for all their endless effort !!!

more about me can be found on my homepage,
especially in my coding blog.
some names mentioned on this site may be trademarks of their respective owners.
thanks to the KaTeX team for their great typesetting library !