Problem 505: Bidirectional Recurrence
View on Project EulerProject Euler Problem 505 Solution
EulerSolve provides an optimized solution for Project Euler Problem 505, Bidirectional Recurrence, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Let \(q=2^{60}\) and \(M=q-1\). The problem builds a masked integer sequence \(u_k\) from a binary recurrence and then asks for a game value \(A(n)\) obtained from a truncated binary descendant tree. For a fixed \(n\), the terminal indices are the integers in \([n,2n-1]\). Each terminal index \(k\) contributes the masked sequence value \(u_k\), and the tree is folded upward by alternating \(\max\) and \(\min\) from the bottom layer toward the root. A direct recursive expansion is infeasible for \(n=10^{12}\). The implementation therefore computes sequence states from the bits of the index, evaluates only a small collection of dyadic blocks, uses complement symmetry on the right side of the terminal band, and applies alpha-beta pruning inside each subtree. Mathematical Approach The key observation is that the sequence generation and the minimax-style tree evaluation can both be written in a form that depends only on binary structure. That makes it possible to skip almost all of the implicit tree....
Detailed mathematical approach
Problem Summary
Let \(q=2^{60}\) and \(M=q-1\). The problem builds a masked integer sequence \(u_k\) from a binary recurrence and then asks for a game value \(A(n)\) obtained from a truncated binary descendant tree.
For a fixed \(n\), the terminal indices are the integers in \([n,2n-1]\). Each terminal index \(k\) contributes the masked sequence value \(u_k\), and the tree is folded upward by alternating \(\max\) and \(\min\) from the bottom layer toward the root.
A direct recursive expansion is infeasible for \(n=10^{12}\). The implementation therefore computes sequence states from the bits of the index, evaluates only a small collection of dyadic blocks, uses complement symmetry on the right side of the terminal band, and applies alpha-beta pruning inside each subtree.
Mathematical Approach
The key observation is that the sequence generation and the minimax-style tree evaluation can both be written in a form that depends only on binary structure. That makes it possible to skip almost all of the implicit tree.
Step 1: Encode the Recurrence as a Two-Component State
Write \(u_0=0\) and \(u_1=1\), and package the current value together with the value at the parent index:
$$s_k=\begin{pmatrix}u_k\\u_{\lfloor k/2\rfloor}\end{pmatrix}.$$
Then the two possible binary extensions of \(k\) are
$$s_{2k}\equiv \begin{pmatrix}3&2\\1&0\end{pmatrix}s_k \pmod{q},\qquad s_{2k+1}\equiv \begin{pmatrix}2&3\\1&0\end{pmatrix}s_k \pmod{q}.$$
So the first coordinate gives
$$u_{2k}\equiv 3u_k+2u_{\lfloor k/2\rfloor}\pmod{q},\qquad u_{2k+1}\equiv 2u_k+3u_{\lfloor k/2\rfloor}\pmod{q}.$$
Because each new bit of the binary expansion of \(k\) selects one of these two matrices, \(u_k\) can be computed by scanning the bits of \(k\) from most significant to least significant. That costs \(O(\log k)\) time instead of filling every previous term.
Step 2: Define the Alternating Subtree Value
For any index \(k\) and any nonnegative height \(d\), define the complete binary subtree value
$$F(k,0)=u_k,$$
$$F(k,d)= \begin{cases} \max\bigl(F(2k,d-1),F(2k+1,d-1)\bigr), & d\text{ odd},\\[4pt] \min\bigl(F(2k,d-1),F(2k+1,d-1)\bigr), & d\text{ even}. \end{cases}$$
This matches the bottom-up pattern used by the implementation: the first layer above leaves takes a maximum, the next layer takes a minimum, and the alternation continues upward.
For a given \(n\), the real terminal band is \([n,2n-1]\), which is usually not a complete power-of-two layer. The algorithm therefore embeds the irregular tree into one complete layer and evaluates only the pieces that matter.
Step 3: Embed the Terminal Band into One Complete Depth
Let
$$h=\left\lfloor \log_2(2n-1)\right\rfloor,\qquad B=2^h.$$
Then
$$B\le 2n-1\lt 2B,$$
so the complete leaf layer at depth \(h\) is the interval \([B,2B-1]\), and the terminal cutoff \(2n\) splits that layer only once.
Every maximal dyadic block in \([B,2B)\) is therefore of one of two types:
$$[j_0,j_0+2^d)\subseteq [B,2n)\quad\text{or}\quad [j_0,j_0+2^d)\subseteq [2n,2B).$$
If a block lies entirely on the left, it corresponds to an ordinary complete subtree. Its value is simply
$$F\!\left(\left\lfloor \frac{j_0}{2^d}\right\rfloor,d\right).$$
Since a single boundary is being decomposed, the number of blocks is only \(O(h)=O(\log n)\).
Step 4: Use Complement Symmetry for Blocks to the Right of \(2n\)
A block lying entirely in \([2n,2B)\) sits one level below a subtree that has already terminated in the original problem. The extra level added by the complete-tree embedding is artificial, and both children of that artificial level carry the same complemented value.
With \(M=q-1\), the right-side block value becomes
$$G(k,0)=M-u_k,$$
$$G(k,d)=M-F(k,d-1)\qquad(d\ge 1).$$
In other words, a right block of height \(d\) can be evaluated as the complement of a genuine subtree of height \(d-1\). This is the symmetry that removes one whole layer from every block lying completely to the right of \(2n\).
Step 5: Evaluate Each Subtree with Alpha-Beta Pruning
The formal definition of \(F(k,d)\) is still exponential in \(d\) if expanded blindly. The implementation therefore uses alpha-beta bounds while recursing through the two children.
At a maximizing node it explores the larger immediate child first; at a minimizing node it explores the smaller immediate child first. That improves the chance that the second branch is provably irrelevant and can be pruned.
This does not change the mathematics, but it changes the practical running time dramatically for the depths that appear in the real input.
Step 6: Recombine the Block Values to Recover the Root
After every maximal dyadic block has a value, the missing parents are rebuilt upward with the same alternation rule:
$$R(s,0)=\text{value of the block starting at }s,$$
$$R(s,d)= \begin{cases} \max\bigl(R(s,d-1),R(s+2^{d-1},d-1)\bigr), & d\text{ odd},\\[4pt] \min\bigl(R(s,d-1),R(s+2^{d-1},d-1)\bigr), & d\text{ even}. \end{cases}$$
Let \(Z=R(0,h)\). The complete-tree embedding and the original problem differ by one final parity flip, so the answer is
$$A(n)= \begin{cases} Z, & h\text{ even},\\[4pt] M-Z, & h\text{ odd}. \end{cases}$$
Worked Example: \(n=10\)
Here
$$h=\left\lfloor \log_2(19)\right\rfloor=4,\qquad B=16,\qquad 2n=20.$$
So the leaf layer \([16,32)\) splits into the maximal dyadic blocks
$$[16,20),\qquad [20,24),\qquad [24,32).$$
The first few sequence values needed here are
$$u_{10}=33,\ u_{11}=27,\ u_{12}=28,\ u_{13}=22,\ u_{14}=25,\ u_{15}=20,$$
$$u_{16}=139,\ u_{17}=111,\ u_{18}=115,\ u_{19}=95.$$
The left block is an ordinary height-2 subtree rooted at \(4\):
$$F(4,2)=\min\bigl(\max(139,111),\max(115,95)\bigr)=115.$$
The middle block is on the right, so one layer collapses:
$$G(5,2)=M-F(5,1)=M-\max(33,27)=M-33.$$
The last block is also on the right:
$$F(3,2)=\min\bigl(\max(28,22),\max(25,20)\bigr)=25,$$
$$G(3,3)=M-F(3,2)=M-25.$$
Now combine the three block values upward:
$$\max(115,M-33)=M-33,$$
$$\min(M-33,M-25)=M-33.$$
Because \(h=4\) is even, no final flip is needed, and therefore
$$A(10)=M-33,$$
which matches the checkpoint used by the implementation.
How the Code Works
The C++ implementation contains the core algorithm. It computes the masked sequence state directly from the binary digits of an index, so obtaining \(u_k\) needs only logarithmic work. The Python and Java implementations delegate to the same compiled logic, so all three languages follow the same mathematics.
Each dyadic block is evaluated independently. Left blocks call the ordinary alternating subtree evaluator \(F(k,d)\); right blocks use the complemented form \(G(k,d)\). Inside each subtree the recursion is guarded by alpha-beta bounds, and the child order is chosen to improve pruning.
Once the block values are known, the implementation stores them by segment position and height, reconstructs any missing parents with alternating \(\max\) and \(\min\), and finally applies the parity correction determined by \(h\). The C++ version also evaluates the independent blocks in parallel, which helps on the largest input.
Complexity Analysis
Computing one sequence value \(u_k\) costs \(O(\log k)\) arithmetic operations because the index is processed bit by bit. The dyadic split of the leaf layer creates only \(O(\log n)\) maximal blocks, and recombining those blocks is also \(O(\log n)\).
The hard part is the subtree evaluation. In the worst case a raw minimax tree of height \(d\) would need \(O(2^d)\) leaf visits, but the implementation avoids most of that cost through two structural reductions: every right-side block loses one full level by complement symmetry, and alpha-beta pruning cuts many recursive branches inside the remaining subtrees. That combination is what makes the target value feasible.
Footnotes and References
- Problem page: https://projecteuler.net/problem=505
- Minimax: Wikipedia - Minimax
- Alpha-beta pruning: Wikipedia - Alpha-beta pruning
- Complete binary tree indexing: Wikipedia - Binary heap
- Dyadic interval decomposition: Wikipedia - Segment tree
Problem 505 source code
C++
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <iostream>
#include <thread>
#include <unordered_map>
#include <vector>
using namespace std;
static constexpr uint64_t MASK = (1ULL << 60) - 1;
struct State {
uint64_t x;
uint64_t parent;
};
struct Block {
uint64_t start;
int depth;
bool right;
uint64_t value;
};
static inline uint64_t combine(uint64_t a, uint64_t b, uint64_t c, uint64_t d) {
unsigned __int128 v = static_cast<unsigned __int128>(a) * b
+ static_cast<unsigned __int128>(c) * d;
return static_cast<uint64_t>(v) & MASK;
}
static State compute_state(uint64_t k) {
if (k == 0) return {0, 0};
uint64_t x = 1;
uint64_t p = 0;
int msb = 63 - __builtin_clzll(k);
for (int i = msb - 1; i >= 0; --i) {
if ((k >> i) & 1ULL) {
uint64_t nx = combine(2, x, 3, p);
p = x;
x = nx;
} else {
uint64_t nx = combine(3, x, 2, p);
p = x;
x = nx;
}
}
return {x, p};
}
static uint64_t minimax(uint64_t x, uint64_t parent, int depth,
uint64_t alpha, uint64_t beta) {
if (depth == 0) return x;
uint64_t left_x = combine(3, x, 2, parent);
uint64_t right_x = combine(2, x, 3, parent);
bool is_max = (depth & 1) != 0;
if (is_max) {
if (left_x < right_x) std::swap(left_x, right_x);
if (depth == 1) return left_x;
uint64_t v = minimax(left_x, x, depth - 1, alpha, beta);
if (v > alpha) alpha = v;
if (alpha >= beta) return alpha;
uint64_t v2 = minimax(right_x, x, depth - 1, alpha, beta);
if (v2 > alpha) alpha = v2;
return alpha;
}
if (left_x > right_x) std::swap(left_x, right_x);
if (depth == 1) return left_x;
uint64_t v = minimax(left_x, x, depth - 1, alpha, beta);
if (v < beta) beta = v;
if (alpha >= beta) return beta;
uint64_t v2 = minimax(right_x, x, depth - 1, alpha, beta);
if (v2 < beta) beta = v2;
return beta;
}
static uint64_t eval_subtree(uint64_t k, int depth) {
State s = compute_state(k);
return minimax(s.x, s.parent, depth, 0, MASK);
}
static void collect_blocks(uint64_t start, int depth, uint64_t left_len,
vector<Block>& blocks) {
uint64_t size = 1ULL << depth;
if (start + size <= left_len) {
blocks.push_back({start, depth, false, 0});
return;
}
if (start >= left_len) {
blocks.push_back({start, depth, true, 0});
return;
}
collect_blocks(start, depth - 1, left_len, blocks);
collect_blocks(start + (size >> 1), depth - 1, left_len, blocks);
}
static uint64_t compute_block_value(const Block& block, uint64_t base) {
uint64_t j0 = base + block.start;
if (!block.right) {
uint64_t k = j0 >> block.depth;
return eval_subtree(k, block.depth);
}
if (block.depth == 0) {
uint64_t k = j0 >> 1;
return MASK - compute_state(k).x;
}
// Right blocks are duplicated complements, so one max level collapses.
uint64_t k = j0 >> block.depth;
return MASK - eval_subtree(k, block.depth - 1);
}
static uint64_t compute_A(uint64_t n, int threads) {
if (n == 1) return 1;
uint64_t total = 2 * n - 1;
int h = 63 - __builtin_clzll(total);
uint64_t base = 1ULL << h;
uint64_t boundary = 2 * n;
uint64_t left_len = boundary - base;
// Split the leaf level into maximal power-of-two blocks on each side.
vector<Block> blocks;
collect_blocks(0, h, left_len, blocks);
if (threads < 1) threads = 1;
if (threads > static_cast<int>(blocks.size())) {
threads = static_cast<int>(blocks.size());
if (threads < 1) threads = 1;
}
atomic<size_t> index(0);
vector<thread> pool;
pool.reserve(threads);
for (int t = 0; t < threads; ++t) {
pool.emplace_back([&]() {
size_t i;
while ((i = index.fetch_add(1)) < blocks.size()) {
blocks[i].value = compute_block_value(blocks[i], base);
}
});
}
for (auto& th : pool) th.join();
unordered_map<uint64_t, uint64_t> values;
values.reserve(blocks.size() * 2);
for (const auto& block : blocks) {
uint64_t key = (block.start << 6) | static_cast<uint64_t>(block.depth);
values.emplace(key, block.value);
}
auto seg = [&](auto&& self, uint64_t start, int depth) -> uint64_t {
uint64_t key = (start << 6) | static_cast<uint64_t>(depth);
auto it = values.find(key);
if (it != values.end()) return it->second;
uint64_t half = 1ULL << (depth - 1);
uint64_t left = self(self, start, depth - 1);
uint64_t right = self(self, start + half, depth - 1);
return (depth & 1) ? max(left, right) : min(left, right);
};
uint64_t z = seg(seg, 0, h);
return (h & 1) ? (MASK - z) : z;
}
static bool run_validation(int threads) {
struct Check {
uint64_t n;
uint64_t expected;
};
const Check checks[] = {
{4, 8},
{10, MASK - 33},
{1000, 101881},
};
bool ok = true;
for (const auto& c : checks) {
uint64_t got = compute_A(c.n, threads);
if (got != c.expected) {
cerr << "Validation failed for n=" << c.n
<< ": got " << got << ", expected " << c.expected << "\n";
ok = false;
}
}
if (ok) {
cerr << "Validation checkpoints passed.\n";
}
return ok;
}
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
uint64_t n = 1000000000000ULL;
unsigned hw = thread::hardware_concurrency();
int threads = hw ? static_cast<int>(hw) : 1;
threads = max(1, min(threads, 8));
bool validate = true;
// Optional CLI: ./a.out [n] [threads] [validate(0/1)]
if (argc >= 2) n = stoull(argv[1]);
if (argc >= 3) threads = max(1, stoi(argv[2]));
if (argc >= 4) validate = (stoi(argv[3]) != 0);
if (validate && !run_validation(min(threads, 2))) {
return 1;
}
uint64_t answer = compute_A(n, threads);
cout << answer << "\n";
return 0;
}
Python
from __future__ import annotations
import re
import shutil
import subprocess
from pathlib import Path
ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")
def parse_output(stdout: str) -> str:
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
if not lines:
return ""
answers = []
equals = []
for line in lines:
m1 = ANSWER_RE.search(line)
if m1:
answers.append(m1.group(1).strip())
m2 = EQUAL_RE.search(line)
if m2:
equals.append(m2.group(1).strip())
if answers:
return answers[-1]
if equals:
return equals[-1]
return lines[-1]
def should_skip_cpp_checkpoints(src: Path) -> bool:
try:
text = src.read_text(encoding="utf-8", errors="ignore")
except OSError:
return False
return "--skip-checkpoints" in text
def run_cpp(binary: Path, src: Path, root: Path) -> str:
cmd = [str(binary)]
if should_skip_cpp_checkpoints(src):
cmd.append("--skip-checkpoints")
try:
return subprocess.check_output(cmd, text=True, cwd=root)
except subprocess.CalledProcessError:
return subprocess.check_output(cmd, text=True, cwd=src.parent)
def solve() -> str:
problem_id = __file__.split("Euler")[-1].split(".")[0]
root = Path(__file__).resolve().parent.parent
src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"
if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
compiler = shutil.which("clang++") or shutil.which("g++")
if not compiler:
raise RuntimeError("No C++ compiler found (clang++/g++).")
subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])
output = run_cpp(binary=binary, src=src, root=root)
parsed = parse_output(output)
if not parsed:
raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
return parsed
if __name__ == "__main__":
print(solve())
Java
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
public class Euler505 {
private static final Pattern ANSWER_RE = Pattern.compile("answer\\s*:\\s*(.+)$", Pattern.CASE_INSENSITIVE);
private static final Pattern EQUAL_RE = Pattern.compile("=\\s*(.+)$");
private static String parseOutput(String stdout) {
String[] lines = stdout.split("\\R");
List<String> nonEmpty = new ArrayList<>();
for (String line : lines) {
String t = line.trim();
if (!t.isEmpty()) {
nonEmpty.add(t);
}
}
if (nonEmpty.isEmpty()) {
return "";
}
List<String> answers = new ArrayList<>();
List<String> equals = new ArrayList<>();
for (String line : nonEmpty) {
Matcher m1 = ANSWER_RE.matcher(line);
if (m1.find()) {
answers.add(m1.group(1).trim());
}
Matcher m2 = EQUAL_RE.matcher(line);
if (m2.find()) {
equals.add(m2.group(1).trim());
}
}
if (!answers.isEmpty()) {
return answers.get(answers.size() - 1);
}
if (!equals.isEmpty()) {
return equals.get(equals.size() - 1);
}
return nonEmpty.get(nonEmpty.size() - 1);
}
private static String pickCompiler() throws Exception {
for (String compiler : List.of("clang++", "g++")) {
Process probe = new ProcessBuilder("bash", "-lc", "command -v " + compiler)
.redirectErrorStream(true)
.start();
String out = new String(probe.getInputStream().readAllBytes());
int rc = probe.waitFor();
if (rc == 0 && !out.trim().isEmpty()) {
return compiler;
}
}
throw new RuntimeException("No C++ compiler found (clang++/g++).");
}
private static Path cppSource(Path root) {
return root.resolve("solutionsCpp").resolve("Euler505.cpp");
}
private static boolean shouldSkipCheckpoints(Path root) {
Path src = cppSource(root);
try {
String text = Files.readString(src);
return text.contains("--skip-checkpoints");
} catch (Exception ex) {
return false;
}
}
private static Path ensureBridgeBinary() throws Exception {
Path root = Paths.get(System.getProperty("user.dir"));
Path src = cppSource(root);
Path bin = root.resolve("solutionsCpp").resolve(".euler505_java_bridge");
boolean rebuild = Files.notExists(bin)
|| Files.getLastModifiedTime(src).compareTo(Files.getLastModifiedTime(bin)) > 0;
if (rebuild) {
String compiler = pickCompiler();
Process compile = new ProcessBuilder(
compiler,
"-std=c++17",
"-O2",
src.toString(),
"-o",
bin.toString())
.inheritIO()
.start();
if (compile.waitFor() != 0) {
throw new RuntimeException("Failed to compile Euler505 C++ bridge.");
}
}
return bin;
}
private static String runBridge(Path bin, Path root, Path srcDir) throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add(bin.toString());
if (shouldSkipCheckpoints(root)) {
cmd.add("--skip-checkpoints");
}
Process first = new ProcessBuilder(cmd)
.directory(root.toFile())
.redirectErrorStream(true)
.start();
String out = new String(first.getInputStream().readAllBytes());
int rc = first.waitFor();
if (rc == 0) {
return out;
}
Process second = new ProcessBuilder(cmd)
.directory(srcDir.toFile())
.redirectErrorStream(true)
.start();
String out2 = new String(second.getInputStream().readAllBytes());
int rc2 = second.waitFor();
if (rc2 == 0) {
return out2;
}
throw new RuntimeException("Euler505 C++ bridge failed.\n" + out + "\n" + out2);
}
private static String solveViaCppBridge() throws Exception {
Path root = Paths.get(System.getProperty("user.dir"));
Path src = cppSource(root);
Path bin = ensureBridgeBinary();
String out = runBridge(bin, root, src.getParent());
String parsed = parseOutput(out);
if (parsed.isEmpty()) {
throw new RuntimeException("Euler505 C++ bridge produced empty output.");
}
return parsed;
}
public static void main(String[] args) throws Exception {
System.out.println(solveViaCppBridge());
}
}