Problem 993: Banana Beaver
View on Project EulerProject Euler Problem 993 Solution
EulerSolve provides an optimized solution for Project Euler Problem 993, Banana Beaver, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary A beaver moves on the infinite integer line. At every step it inspects only the two cells \(x\) and \(x+1\), where \(x\) is its current position. Depending on whether those cells contain bananas, it removes a banana, moves a banana one cell left, or, if both cells are empty and it still carries at least three bananas, drops three bananas at \(x-1,x,x+1\) and jumps two cells left. The requested function \(\operatorname{BB}(N)\) is the final position when the beaver starts at \(0\) carrying \(N\) bananas and the line is initially empty. The official check value is $$\operatorname{BB}(1000)=1499.$$ The direct state space is infinite because the line is unbounded and \(N=10^{18}\) is far too large for step-by-step simulation. The solution below reduces the game to a finite universal simulation and then uses an eventual affine period in a threshold sequence. Mathematical Approach 1. Conservation Law Let \(C\) be the number of bananas carried by the beaver and let \(L\) be the number of bananas currently lying on the number line. Every rule preserves the total number of bananas: $$C+L=N.$$ Rules that pick up a banana decrease \(L\) by \(1\) and increase \(C\) by \(1\). The rule that moves a banana from \(x+1\) to \(x\) keeps \(L\) unchanged. The dropping rule decreases \(C\) by \(3\) and increases \(L\) by \(3\)....
Detailed mathematical approach
Problem Summary
A beaver moves on the infinite integer line. At every step it inspects only the two cells \(x\) and \(x+1\), where \(x\) is its current position. Depending on whether those cells contain bananas, it removes a banana, moves a banana one cell left, or, if both cells are empty and it still carries at least three bananas, drops three bananas at \(x-1,x,x+1\) and jumps two cells left.
The requested function \(\operatorname{BB}(N)\) is the final position when the beaver starts at \(0\) carrying \(N\) bananas and the line is initially empty. The official check value is
$$\operatorname{BB}(1000)=1499.$$
The direct state space is infinite because the line is unbounded and \(N=10^{18}\) is far too large for step-by-step simulation. The solution below reduces the game to a finite universal simulation and then uses an eventual affine period in a threshold sequence.
Mathematical Approach
1. Conservation Law
Let \(C\) be the number of bananas carried by the beaver and let \(L\) be the number of bananas currently lying on the number line. Every rule preserves the total number of bananas:
$$C+L=N.$$
Rules that pick up a banana decrease \(L\) by \(1\) and increase \(C\) by \(1\). The rule that moves a banana from \(x+1\) to \(x\) keeps \(L\) unchanged. The dropping rule decreases \(C\) by \(3\) and increases \(L\) by \(3\). Hence the carried count never needs to be stored explicitly if we know \(N\) and \(L\).
2. When Does a Game with \(N\) Bananas Stop?
The game can stop only at an empty-pair event, meaning that there is no banana at either \(x\) or \(x+1\). At such an event the beaver checks whether \(C \ge 3\). Using the invariant, this is
$$N-L\ge 3.$$
Therefore the game stops precisely when
$$N-L<3 \quad\Longleftrightarrow\quad L\ge N-2.$$
This is the key reduction: instead of simulating a separate game for every \(N\), simulate a single beaver that always performs the three-banana drop, and record the first position at which the line contains at least a specified number of bananas.
3. Universal Simulator and Threshold Function
Define \(F(t)\) to be the current position \(x\) at the first empty-pair event for which the number \(L\) of bananas on the line is at least \(t\). Because a game with \(N\) bananas stops at the first empty-pair event with \(L\ge N-2\), we have
$$\boxed{\operatorname{BB}(N)=F(\max(0,N-2)).}$$
The code computes this function with Simulator::first_positions. Whenever an empty-pair
event is reached, the current \(L\) may skip several thresholds, so all still-unfilled entries up to
\(L\) receive the same position \(x\). This is why the implementation uses a loop
$$\text{while filled} < \min(L,T):\quad F(\text{filled}+1)=x.$$
The simulated line is stored as a Boolean array with a large offset. The helper functions
set and clear update the array and maintain \(L\) exactly.
4. Empirical Period as a Finite Certificate
After a transient prefix the threshold sequence becomes affine-periodic. The constants used by the program are
$$a=512,\qquad p=71,\qquad s=118.$$
For every checked threshold \(m\) in the range
$$512\le m\le 10000-71,$$
the program verifies
$$\boxed{F(m+71)=F(m)+118.}$$
This is stronger than checking only the final answer: it checks the whole post-transient segment generated by the finite simulation. Conceptually, this is a cycle in the normalized local configuration: after \(71\) additional line bananas, the same local pattern reappears shifted by \(118\) cells.
5. Extrapolating to \(10^{18}\)
Let
$$t=\max(0,N-2).$$
If \(t\) lies inside the precomputed table, the answer is simply \(F(t)\). Otherwise write
$$q=\left\lfloor {t-a\over p}\right\rfloor,\qquad r=a+((t-a)\bmod p).$$
Repeatedly applying the affine-periodic identity gives
$$\boxed{\operatorname{BB}(N)=F(r)+q\,s.}$$
For the target \(N=10^{18}\), the code uses \(t=10^{18}-2\), \(q=14084507042253513\), and \(r=575\). The stored table supplies \(F(r)\), and the final decimal value is printed by the program.
6. Worked Checkpoint: \(N=1000\)
Here \(t=N-2=998\). The period formula gives
$$q=\left\lfloor {998-512\over 71}\right\rfloor=6,\qquad r=512+((998-512)\bmod 71)=572.$$
The finite simulator gives \(F(572)=791\). Therefore
$$F(998)=F(572)+6\cdot 118=791+708=1499,$$
which matches the official checkpoint.
How the Code Works
The C++ program first builds \(F(0),F(1),\dots,F(10000)\). It then runs small correctness checkpoints: \(\operatorname{BB}(0)=\operatorname{BB}(1)=\operatorname{BB}(2)=0\), \(\operatorname{BB}(3)=1\), \(\operatorname{BB}(5)=-1\), and \(\operatorname{BB}(1000)=1499\). It also checks the affine period \(F(m+71)=F(m)+118\) throughout the post-transient range.
The Python and Java versions use the same constants, the same threshold table, and the same
extrapolation formula. The only implementation-level difference is numeric representation: Python
integers are arbitrary precision, Java uses long, and C++ prints a
__int128_t value through a small decimal conversion helper.
Complexity Analysis
Let \(T=10000\) be the table limit used by the implementation. The universal simulation is finite and stores \(O(T)\) cells in the Boolean window. Once the table and the period check are complete, a query for any \(N\), including \(10^{18}\), is answered by a constant number of integer operations.
Thus the target computation has fixed precomputation cost and \(O(1)\) extrapolation time. A naive simulation up to \(N=10^{18}\) would be infeasible because it would need to wait for an enormous number of drop and pickup events.
References
- Problem page: Project Euler 993
- Finite-state cycle detection: Wikipedia - Cycle detection
- Cellular automata and local update rules: Wikipedia - Cellular automaton
Problem 993 source code
C++
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
namespace {
using i64 = std::int64_t;
using u64 = std::uint64_t;
using i128 = __int128_t;
constexpr u64 TARGET_N = 1'000'000'000'000'000'000ULL;
constexpr int CYCLE_START = 512;
constexpr int CYCLE_BANANAS = 71;
constexpr int CYCLE_SHIFT = 118;
constexpr int CHECK_LIMIT = 10'000;
struct Simulator {
explicit Simulator(const int max_target)
: offset(20 * max_target + 10'000),
occupied(static_cast<std::size_t>(2 * offset + 10), 0) {}
std::vector<i64> first_positions(const int max_target) {
std::vector<i64> first(static_cast<std::size_t>(max_target + 1), 0);
int filled = -1;
while (filled < max_target) {
const bool here = has(x);
const bool next = has(x + 1);
if (here && next) {
clear(x + 1);
--x;
} else if (here) {
clear(x);
x += 2;
} else if (next) {
clear(x + 1);
set(x);
x += 2;
} else {
while (filled < std::min(line_count, max_target)) {
first[static_cast<std::size_t>(++filled)] = x;
}
assert(!has(x - 1) && !has(x) && !has(x + 1));
set(x - 1);
set(x);
set(x + 1);
x -= 2;
}
}
return first;
}
private:
int offset;
std::vector<unsigned char> occupied;
i64 x = 0;
int line_count = 0;
std::size_t index(const i64 position) const {
const i64 idx = position + static_cast<i64>(offset);
assert(0 <= idx && idx < static_cast<i64>(occupied.size()));
return static_cast<std::size_t>(idx);
}
bool has(const i64 position) const {
return occupied[index(position)] != 0;
}
void set(const i64 position) {
unsigned char& cell = occupied[index(position)];
assert(cell == 0);
cell = 1;
++line_count;
}
void clear(const i64 position) {
unsigned char& cell = occupied[index(position)];
assert(cell != 0);
cell = 0;
--line_count;
}
};
std::string to_string_i128(i128 value) {
if (value == 0) {
return "0";
}
bool negative = false;
if (value < 0) {
negative = true;
value = -value;
}
std::string result;
while (value > 0) {
result.push_back(static_cast<char>('0' + value % 10));
value /= 10;
}
if (negative) {
result.push_back('-');
}
std::reverse(result.begin(), result.end());
return result;
}
i128 bb(const u64 n, const std::vector<i64>& first) {
const u64 target = n <= 2 ? 0 : n - 2;
if (target < first.size()) {
return first[static_cast<std::size_t>(target)];
}
const u64 cycles = (target - CYCLE_START) / CYCLE_BANANAS;
const int residue = CYCLE_START + static_cast<int>((target - CYCLE_START) % CYCLE_BANANAS);
return static_cast<i128>(first[static_cast<std::size_t>(residue)]) +
static_cast<i128>(cycles) * CYCLE_SHIFT;
}
void run_checkpoints(const std::vector<i64>& first) {
assert(bb(0, first) == 0);
assert(bb(1, first) == 0);
assert(bb(2, first) == 0);
assert(bb(3, first) == 1);
assert(bb(5, first) == -1);
assert(bb(1000, first) == 1499);
for (int m = CYCLE_START; m + CYCLE_BANANAS <= CHECK_LIMIT; ++m) {
assert(first[static_cast<std::size_t>(m + CYCLE_BANANAS)] ==
first[static_cast<std::size_t>(m)] + CYCLE_SHIFT);
}
}
} // namespace
int main() {
Simulator simulator(CHECK_LIMIT);
const std::vector<i64> first = simulator.first_positions(CHECK_LIMIT);
run_checkpoints(first);
std::cout << to_string_i128(bb(TARGET_N, first)) << '\n';
return 0;
}
Python
import sys
TARGET_N = 1_000_000_000_000_000_000
CYCLE_START = 512
CYCLE_BANANAS = 71
CYCLE_SHIFT = 118
CHECK_LIMIT = 10_000
class Simulator:
def __init__(self, max_target):
self.offset = 20 * max_target + 10_000
self.occupied = [False] * (2 * self.offset + 10)
self.x = 0
self.line_count = 0
def _index(self, position):
idx = position + self.offset
assert 0 <= idx < len(self.occupied)
return idx
def _has(self, position):
return self.occupied[self._index(position)]
def _set(self, position):
idx = self._index(position)
assert not self.occupied[idx]
self.occupied[idx] = True
self.line_count += 1
def _clear(self, position):
idx = self._index(position)
assert self.occupied[idx]
self.occupied[idx] = False
self.line_count -= 1
def first_positions(self, max_target):
first = [0] * (max_target + 1)
filled = -1
while filled < max_target:
here = self._has(self.x)
nxt = self._has(self.x + 1)
if here and nxt:
self._clear(self.x + 1)
self.x -= 1
elif here:
self._clear(self.x)
self.x += 2
elif nxt:
self._clear(self.x + 1)
self._set(self.x)
self.x += 2
else:
stop = min(self.line_count, max_target)
while filled < stop:
filled += 1
first[filled] = self.x
assert not self._has(self.x - 1)
assert not self._has(self.x)
assert not self._has(self.x + 1)
self._set(self.x - 1)
self._set(self.x)
self._set(self.x + 1)
self.x -= 2
return first
def bb(n, first):
target = 0 if n <= 2 else n - 2
if target < len(first):
return first[target]
cycles = (target - CYCLE_START) // CYCLE_BANANAS
residue = CYCLE_START + (target - CYCLE_START) % CYCLE_BANANAS
return first[residue] + cycles * CYCLE_SHIFT
def run_checkpoints(first):
assert bb(0, first) == 0
assert bb(1, first) == 0
assert bb(2, first) == 0
assert bb(3, first) == 1
assert bb(5, first) == -1
assert bb(1000, first) == 1499
for m in range(CYCLE_START, CHECK_LIMIT - CYCLE_BANANAS + 1):
assert first[m + CYCLE_BANANAS] == first[m] + CYCLE_SHIFT
def main(argv=None):
argv = sys.argv if argv is None else argv
should_run_checkpoints = True
for arg in argv[1:]:
if arg == "--skip-checkpoints":
should_run_checkpoints = False
continue
print(f"Unknown argument: {arg}", file=sys.stderr)
return 1
simulator = Simulator(CHECK_LIMIT)
first = simulator.first_positions(CHECK_LIMIT)
if should_run_checkpoints:
run_checkpoints(first)
print(bb(TARGET_N, first))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Java
public class Euler993 {
private static final long TARGET_N = 1_000_000_000_000_000_000L;
private static final int CYCLE_START = 512;
private static final int CYCLE_BANANAS = 71;
private static final int CYCLE_SHIFT = 118;
private static final int CHECK_LIMIT = 10_000;
private static final class Simulator {
private final int offset;
private final boolean[] occupied;
private long x = 0L;
private int lineCount = 0;
private Simulator(int maxTarget) {
offset = 20 * maxTarget + 10_000;
occupied = new boolean[2 * offset + 10];
}
private int index(long position) {
long idx = position + offset;
if (idx < 0L || idx >= occupied.length) {
throw new IllegalStateException("Position outside simulation window: " + position);
}
return (int) idx;
}
private boolean has(long position) {
return occupied[index(position)];
}
private void set(long position) {
int idx = index(position);
if (occupied[idx]) {
throw new IllegalStateException("Cell already occupied: " + position);
}
occupied[idx] = true;
lineCount += 1;
}
private void clear(long position) {
int idx = index(position);
if (!occupied[idx]) {
throw new IllegalStateException("Cell already empty: " + position);
}
occupied[idx] = false;
lineCount -= 1;
}
private long[] firstPositions(int maxTarget) {
long[] first = new long[maxTarget + 1];
int filled = -1;
while (filled < maxTarget) {
boolean here = has(x);
boolean next = has(x + 1);
if (here && next) {
clear(x + 1);
x -= 1;
} else if (here) {
clear(x);
x += 2;
} else if (next) {
clear(x + 1);
set(x);
x += 2;
} else {
int stop = Math.min(lineCount, maxTarget);
while (filled < stop) {
filled += 1;
first[filled] = x;
}
if (has(x - 1) || has(x) || has(x + 1)) {
throw new IllegalStateException("Drop cells are not empty");
}
set(x - 1);
set(x);
set(x + 1);
x -= 2;
}
}
return first;
}
}
private static long bb(long n, long[] first) {
long target = n <= 2L ? 0L : n - 2L;
if (target < first.length) {
return first[(int) target];
}
long cycles = (target - CYCLE_START) / CYCLE_BANANAS;
int residue = CYCLE_START + (int) ((target - CYCLE_START) % CYCLE_BANANAS);
return first[residue] + cycles * CYCLE_SHIFT;
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new IllegalStateException(message);
}
}
private static void runCheckpoints(long[] first) {
require(bb(0L, first) == 0L, "Checkpoint failed for BB(0)");
require(bb(1L, first) == 0L, "Checkpoint failed for BB(1)");
require(bb(2L, first) == 0L, "Checkpoint failed for BB(2)");
require(bb(3L, first) == 1L, "Checkpoint failed for BB(3)");
require(bb(5L, first) == -1L, "Checkpoint failed for BB(5)");
require(bb(1000L, first) == 1499L, "Checkpoint failed for BB(1000)");
for (int m = CYCLE_START; m + CYCLE_BANANAS <= CHECK_LIMIT; ++m) {
require(first[m + CYCLE_BANANAS] == first[m] + CYCLE_SHIFT, "Cycle checkpoint failed at " + m);
}
}
public static void main(String[] args) {
boolean shouldRunCheckpoints = true;
for (String arg : args) {
if ("--skip-checkpoints".equals(arg)) {
shouldRunCheckpoints = false;
continue;
}
System.err.println("Unknown argument: " + arg);
System.exit(1);
}
Simulator simulator = new Simulator(CHECK_LIMIT);
long[] first = simulator.firstPositions(CHECK_LIMIT);
if (shouldRunCheckpoints) {
runCheckpoints(first);
}
System.out.println(bb(TARGET_N, first));
}
}