Problem 311: Biclinic Integral Quadrilaterals
View on Project EulerProject Euler Problem 311 Solution
EulerSolve provides an optimized solution for Project Euler Problem 311, Biclinic Integral Quadrilaterals, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Write $$r=AO=CO,\qquad s=BO=DO=\frac{BD}{2},\qquad 0<r\le s.$$ Then every biclinic quadrilateral lives on one fixed pair of concentric circles centered at \(O\): the vertices \(A,C\) lie on radius \(r\), while \(B,D\) are the opposite endpoints of a diameter of radius \(s\). The key invariant is $$AB^2+BC^2+CD^2+AD^2=4(r^2+s^2).$$ So if we define $$n=r^2+s^2,$$ then the condition in the problem is simply \(n\le N/4\). The whole problem becomes: for each \(n\), how many biclinic quadrilaterals are produced by the representations of \(n\) as a sum of two squares? Mathematical Approach 1) Every side pair gives a strict representation of the same \(n\). Look at vertex \(A\). Let $$u=AB,\qquad v=AD,\qquad u<v.$$ Define $$x=\frac{u+v}{2},\qquad y=\frac{v-u}{2},\qquad u=x-y,\quad v=x+y.$$ In triangle \(BAD\), the point \(O\) is the midpoint of \(BD\). Apollonius gives $$u^2+v^2=2(AO^2+BO^2)=2(r^2+s^2)=2n.$$ But also $$u^2+v^2=(x-y)^2+(x+y)^2=2(x^2+y^2).$$ Therefore $$x^2+y^2=n.$$ Because \(u\) and \(v\) are distinct positive integers, we get a strict representation \(x>y>0\). The same argument at vertex \(C\) gives another strict representation of the same \(n\). 2) The reverse construction. Suppose we already know one representation $$n=s^2+r^2,\qquad s\ge r>0,$$ which will be used for the diagonal data \(BD=2s\) and \(AO=CO=r\)....
Detailed mathematical approach
Problem Summary
Write
$$r=AO=CO,\qquad s=BO=DO=\frac{BD}{2},\qquad 0<r\le s.$$
Then every biclinic quadrilateral lives on one fixed pair of concentric circles centered at \(O\): the vertices \(A,C\) lie on radius \(r\), while \(B,D\) are the opposite endpoints of a diameter of radius \(s\).
The key invariant is
$$AB^2+BC^2+CD^2+AD^2=4(r^2+s^2).$$
So if we define
$$n=r^2+s^2,$$
then the condition in the problem is simply \(n\le N/4\). The whole problem becomes: for each \(n\), how many biclinic quadrilaterals are produced by the representations of \(n\) as a sum of two squares?
Mathematical Approach
1) Every side pair gives a strict representation of the same \(n\).
Look at vertex \(A\). Let
$$u=AB,\qquad v=AD,\qquad u<v.$$
Define
$$x=\frac{u+v}{2},\qquad y=\frac{v-u}{2},\qquad u=x-y,\quad v=x+y.$$
In triangle \(BAD\), the point \(O\) is the midpoint of \(BD\). Apollonius gives
$$u^2+v^2=2(AO^2+BO^2)=2(r^2+s^2)=2n.$$
But also
$$u^2+v^2=(x-y)^2+(x+y)^2=2(x^2+y^2).$$
Therefore
$$x^2+y^2=n.$$
Because \(u\) and \(v\) are distinct positive integers, we get a strict representation \(x>y>0\). The same argument at vertex \(C\) gives another strict representation of the same \(n\).
2) The reverse construction.
Suppose we already know one representation
$$n=s^2+r^2,\qquad s\ge r>0,$$
which will be used for the diagonal data \(BD=2s\) and \(AO=CO=r\). Now take another strict representation
$$n=x^2+y^2,\qquad x>y>0.$$
Set
$$u=x-y,\qquad v=x+y.$$
Then
$$u^2+v^2=2n=2(r^2+s^2),$$
so by Apollonius any point \(P\) with \(PB=u\) and \(PD=v\) automatically satisfies \(PO=r\). Thus the pair \((x,y)\) determines a valid vertex on the inner circle.
To make the circles intersect, we need the triangle inequalities with base \(BD=2s\):
$$v-u=2y<2s<2x=u+v.$$
This is exactly why the diagonal representation must be the smallest one in the ordering below.
3) Order the representations of \(n\).
Let
$$\mathcal R(n)=\{(x,y)\in\mathbb Z_{>0}^2:\ x>y,\ x^2+y^2=n\},\qquad m(n)=|\mathcal R(n)|.$$
Sort the pairs by increasing \(x\):
$$x_1<x_2<\cdots<x_{m(n)}.$$
Because \(x_i^2+y_i^2=n\), the corresponding \(y_i\) values strictly decrease:
$$y_1>y_2>\cdots>y_{m(n)}.$$
On the branch \(x>\sqrt{n/2}\), the two functions
$$x-y\quad\text{and}\quad x+y$$
behave monotonically in opposite directions:
$$x-y\ \text{increases},\qquad x+y\ \text{decreases}.$$
So if we choose three strict representations
$$ (x_i,y_i),\ (x_j,y_j),\ (x_k,y_k)\qquad (i<j<k),$$
then the smallest one \((x_i,y_i)\) is the only possible diagonal pair \((s,r)\), because it guarantees \(x_j>s\) and \(x_k>s\), while the reverse choice would violate the triangle inequality.
4) One 3-subset of representations gives one quadrilateral.
Take the diagonal data from the smallest representation:
$$s=x_i,\qquad r=y_i,\qquad BD=2x_i,\qquad AO=CO=y_i.$$
Use the next two representations for the two vertices:
$$AB=x_j-y_j,\qquad AD=x_j+y_j,$$
$$BC=x_k-y_k,\qquad CD=x_k+y_k.$$
The monotonicity above gives the strict side order automatically:
$$AB<BC<CD<AD.$$
Hence every 3-element subset of \(\mathcal R(n)\) contributes exactly one biclinic integral quadrilateral. This yields the generic term
$$\binom{m(n)}{3}.$$
5) The special case \(n=2t^2\).
If
$$n=2t^2,$$
then besides the strict representations there is also the symmetric one \((t,t)\). It is not counted in \(m(n)\) because it is not strict, but it is perfectly valid for the diagonal data:
$$s=t,\qquad r=t,\qquad AO=BO=CO=DO=t.$$
Once this diagonal pair is fixed, any two strict representations of \(n\) can play the roles of the two vertices, so this case contributes
$$\binom{m(n)}{2}.$$
6) Final counting formula.
Putting the generic and special cases together,
$$B(N)=\sum_{1\le n\le N/4}\left[\binom{m(n)}{3}+\mathbf 1_{\,n=2t^2}\binom{m(n)}{2}\right].$$
Worked Examples
Official sample. The example in the statement has
$$AO=CO=23,\qquad BO=DO=24,$$
so
$$n=23^2+24^2=1105.$$
The strict representations of \(1105\) are
$$1105=24^2+23^2=31^2+12^2=32^2+9^2=33^2+4^2.$$
Choose \((24,23)\) as the diagonal pair. Then using \((31,12)\) and \((33,4)\) for the two vertices gives
$$AB=31-12=19,\qquad AD=31+12=43,$$
$$BC=33-4=29,\qquad CD=33+4=37,$$
and
$$BD=2\cdot 24=48,\qquad AO=CO=23.$$
This is exactly the quadrilateral from the problem statement. Since \(m(1105)=4\), the value \(n=1105\) actually contributes \(\binom{4}{3}=4\) quadrilaterals in total.
Why the extra \(\binom{m(n)}{2}\) term exists. Consider
$$1250=25^2+25^2=31^2+17^2=35^2+5^2.$$
Here \(m(1250)=2\) because only the two strict representations are counted. The generic term is \(\binom{2}{3}=0\), but the symmetric pair \((25,25)\) can be used as the diagonal representation, so we get one extra quadrilateral:
$$\binom{2}{2}=1.$$
Algorithm
1) Work blockwise in \(n\). Let \(T=N/4\). The code processes intervals
$$[L,H]\subseteq [1,T]$$
of width \(W\). For one block it stores a local counter array `counts[n-L]`.
2) Enumerate all strict representations in the block.
For every \(y\ge 1\), the smallest possible strict value is
$$y^2+(y+1)^2.$$
If that already exceeds \(H\), we stop. Otherwise the feasible \(x\)-range is
$$x_{\min}=\max\!\left(y+1,\left\lceil \sqrt{L-y^2}\right\rceil\right),\qquad x_{\max}=\left\lfloor \sqrt{H-y^2}\right\rfloor.$$
Every pair \((x,y)\) with \(x_{\min}\le x\le x_{\max}\) contributes one strict representation to
$$n=x^2+y^2.$$
3) Convert representation counts into quadrilateral counts.
After the block is filled, every touched value \(n\) has \(m(n)\) stored in its counter. The code adds
$$\binom{m(n)}{3}$$
and, if \(n/2\) is a square, also
$$\binom{m(n)}{2}.$$
Then the touched counters are reset to zero and the next block is processed.
4) Why the implementation is fast.
It never factors every integer \(n\). It only enumerates lattice points \((x,y)\) that actually satisfy \(x^2+y^2\le T\). The `touched` list ensures that only entries hit in the current block are visited when the block is finalized.
Complexity Analysis
Let \(T=N/4\). The memory usage is \(O(W)\) for block width \(W\). The running time is essentially proportional to the number of strict lattice representations
$$x^2+y^2\le T,\qquad x>y>0,$$
that are generated across all blocks. The code also parallelizes naturally because different blocks are independent.
Checks And Final Result
The C++ implementation verifies
$$B(10\,000)=49,\qquad B(1\,000\,000)=38239,$$
and the default full run returns
$$B(10\,000\,000\,000)=2466018557.$$
Further Reading
- Problem page: https://projecteuler.net/problem=311
- Apollonius's theorem: https://en.wikipedia.org/wiki/Apollonius's_theorem
- Sum of two squares theorem: https://en.wikipedia.org/wiki/Fermat's_theorem_on_sums_of_two_squares
Problem 311 source code
C++
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace {
using u64 = std::uint64_t;
using u32 = std::uint32_t;
using u16 = std::uint16_t;
using u128 = unsigned __int128;
constexpr u64 kDefaultLimit = 10'000'000'000ULL;
constexpr u64 kDefaultBlockSpan = 4'000'000ULL;
constexpr u64 kCheckpointLimit1 = 10'000ULL;
constexpr u64 kCheckpointExpected1 = 49ULL;
constexpr u64 kCheckpointLimit2 = 1'000'000ULL;
constexpr u64 kCheckpointExpected2 = 38'239ULL;
constexpr u64 kThreadConsistencyLimit = 4'000'000ULL;
struct Options {
u64 limit = kDefaultLimit;
u64 block_span = kDefaultBlockSpan;
bool allow_multithreading = true;
bool run_checkpoints = true;
unsigned requested_threads = 0U;
};
bool parse_u64_after_prefix(const std::string& arg, const char* prefix, u64& value) {
const std::string p(prefix);
if (arg.rfind(p, 0) != 0) {
return false;
}
const std::string tail = arg.substr(p.size());
if (tail.empty()) {
return false;
}
u64 parsed = 0ULL;
for (const char c : tail) {
if (c < '0' || c > '9') {
return false;
}
const u64 digit = static_cast<u64>(c - '0');
if (parsed > (std::numeric_limits<u64>::max() - digit) / 10ULL) {
return false;
}
parsed = parsed * 10ULL + digit;
}
value = parsed;
return true;
}
bool parse_unsigned_after_prefix(const std::string& arg,
const char* prefix,
unsigned& value) {
u64 parsed = 0ULL;
if (!parse_u64_after_prefix(arg, prefix, parsed)) {
return false;
}
if (parsed > static_cast<u64>(std::numeric_limits<unsigned>::max())) {
return false;
}
value = static_cast<unsigned>(parsed);
return true;
}
bool parse_arguments(int argc, char** argv, Options& options) {
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--single-thread") {
options.allow_multithreading = false;
continue;
}
if (arg == "--skip-checkpoints") {
options.run_checkpoints = false;
continue;
}
u64 parsed_u64 = 0ULL;
if (parse_u64_after_prefix(arg, "--limit=", parsed_u64)) {
options.limit = parsed_u64;
continue;
}
if (parse_u64_after_prefix(arg, "--block=", parsed_u64)) {
options.block_span = parsed_u64;
continue;
}
unsigned parsed_unsigned = 0U;
if (parse_unsigned_after_prefix(arg, "--threads=", parsed_unsigned)) {
options.requested_threads = parsed_unsigned;
continue;
}
std::cerr << "Unknown argument: " << arg << '\n';
return false;
}
if (options.block_span == 0ULL) {
std::cerr << "--block must be at least 1.\n";
return false;
}
return true;
}
u64 isqrt_u64(u64 n) {
if (n == 0ULL) {
return 0ULL;
}
u64 r = static_cast<u64>(std::sqrt(static_cast<long double>(n)));
while ((r + 1ULL) <= n / (r + 1ULL)) {
++r;
}
while (r > n / r) {
--r;
}
return r;
}
u64 isqrt_ceil_u64(u64 n) {
const u64 r = isqrt_u64(n);
return (r * r == n) ? r : (r + 1ULL);
}
bool is_twice_square(u64 n) {
if ((n & 1ULL) != 0ULL) {
return false;
}
const u64 m = n >> 1ULL;
const u64 r = isqrt_u64(m);
return r * r == m;
}
u64 n_choose_2(u64 n) {
return (n < 2ULL) ? 0ULL : (n * (n - 1ULL) / 2ULL);
}
u64 n_choose_3(u64 n) {
if (n < 3ULL) {
return 0ULL;
}
const u128 numerator = static_cast<u128>(n) * (n - 1ULL) * (n - 2ULL);
return static_cast<u64>(numerator / 6ULL);
}
unsigned choose_thread_count(bool allow_multithreading,
unsigned requested_threads,
std::size_t workload) {
if (!allow_multithreading || workload < 2ULL) {
return 1U;
}
unsigned threads = requested_threads;
if (threads == 0U) {
threads = std::thread::hardware_concurrency();
if (threads == 0U) {
threads = 1U;
}
}
return std::max(1U, std::min<unsigned>(threads, static_cast<unsigned>(workload)));
}
u64 process_block(u64 low,
u64 high,
std::vector<u16>& counts,
std::vector<u32>& touched) {
touched.clear();
for (u64 y = 1ULL;; ++y) {
const u64 y2 = y * y;
const u64 min_n = y2 + (y + 1ULL) * (y + 1ULL);
if (min_n > high) {
break;
}
u64 x_min = y + 1ULL;
const u64 x_min_n = y2 + x_min * x_min;
if (x_min_n < low) {
x_min = isqrt_ceil_u64(low - y2);
if (x_min <= y) {
x_min = y + 1ULL;
}
}
const u64 x_max = isqrt_u64(high - y2);
if (x_max < x_min) {
continue;
}
u64 x2 = x_min * x_min;
for (u64 x = x_min; x <= x_max; ++x) {
const u64 n = y2 + x2;
const u32 idx = static_cast<u32>(n - low);
u16& count = counts[static_cast<std::size_t>(idx)];
if (count == 0U) {
touched.push_back(idx);
}
if (count == std::numeric_limits<u16>::max()) {
throw std::runtime_error("Representation count overflowed uint16_t.");
}
++count;
x2 += (2ULL * x + 1ULL);
}
}
u64 block_sum = 0ULL;
for (const u32 idx : touched) {
u16& count = counts[static_cast<std::size_t>(idx)];
const u64 m = static_cast<u64>(count);
if (m >= 2ULL) {
const u64 n = low + static_cast<u64>(idx);
block_sum += n_choose_3(m);
if (is_twice_square(n)) {
block_sum += n_choose_2(m);
}
}
count = 0U;
}
return block_sum;
}
u64 solve_biclinic(u64 limit,
bool allow_multithreading,
unsigned requested_threads,
u64 block_span) {
if (limit < 4ULL) {
return 0ULL;
}
const u64 max_t = limit / 4ULL;
const u64 block_count_u64 = (max_t + block_span - 1ULL) / block_span;
if (block_count_u64 > static_cast<u64>(std::numeric_limits<std::size_t>::max())) {
throw std::runtime_error("Too many blocks for this platform.");
}
const std::size_t block_count = static_cast<std::size_t>(block_count_u64);
const unsigned threads =
choose_thread_count(allow_multithreading, requested_threads, block_count);
std::vector<u64> partial(threads, 0ULL);
auto worker = [&](unsigned tid) {
std::vector<u16> counts(static_cast<std::size_t>(block_span), 0U);
std::vector<u32> touched;
touched.reserve(static_cast<std::size_t>(block_span / 4ULL + 1024ULL));
u64 local_sum = 0ULL;
for (std::size_t bi = tid; bi < block_count; bi += threads) {
const u64 low = 1ULL + static_cast<u64>(bi) * block_span;
const u64 high = std::min<u64>(max_t, low + block_span - 1ULL);
local_sum += process_block(low, high, counts, touched);
}
partial[tid] = local_sum;
};
std::vector<std::thread> pool;
pool.reserve(threads > 0U ? threads - 1U : 0U);
for (unsigned t = 1U; t < threads; ++t) {
pool.emplace_back(worker, t);
}
worker(0U);
for (std::thread& thread : pool) {
thread.join();
}
u64 total = 0ULL;
for (const u64 x : partial) {
total += x;
}
return total;
}
bool run_checkpoints(const Options& options) {
const u64 sample1 = solve_biclinic(
kCheckpointLimit1, options.allow_multithreading, options.requested_threads, options.block_span);
if (sample1 != kCheckpointExpected1) {
std::cerr << "Checkpoint failed for N=" << kCheckpointLimit1
<< ": expected " << kCheckpointExpected1
<< ", got " << sample1 << '\n';
return false;
}
std::cout << "Checkpoint passed: B(" << kCheckpointLimit1 << ") = " << sample1 << '\n';
const u64 sample2 = solve_biclinic(
kCheckpointLimit2, options.allow_multithreading, options.requested_threads, options.block_span);
if (sample2 != kCheckpointExpected2) {
std::cerr << "Checkpoint failed for N=" << kCheckpointLimit2
<< ": expected " << kCheckpointExpected2
<< ", got " << sample2 << '\n';
return false;
}
std::cout << "Checkpoint passed: B(" << kCheckpointLimit2 << ") = " << sample2 << '\n';
if (options.allow_multithreading) {
const u64 multi = solve_biclinic(
kThreadConsistencyLimit, true, options.requested_threads, options.block_span);
const u64 single = solve_biclinic(
kThreadConsistencyLimit, false, options.requested_threads, options.block_span);
if (multi != single) {
std::cerr << "Thread consistency failed at N=" << kThreadConsistencyLimit
<< ": multi-thread=" << multi
<< ", single-thread=" << single << '\n';
return false;
}
std::cout << "Checkpoint passed: thread consistency at N="
<< kThreadConsistencyLimit << '\n';
}
return true;
}
} // namespace
int main(int argc, char** argv) {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
Options options;
if (!parse_arguments(argc, argv, options)) {
return 1;
}
try {
const auto start_time = std::chrono::steady_clock::now();
if (options.run_checkpoints && !run_checkpoints(options)) {
return 1;
}
const u64 result = solve_biclinic(options.limit,
options.allow_multithreading,
options.requested_threads,
options.block_span);
const auto end_time = std::chrono::steady_clock::now();
const std::chrono::duration<double> elapsed = end_time - start_time;
std::cout << "B(" << options.limit << ") = " << result << '\n';
std::cout << "Elapsed: " << elapsed.count() << " seconds\n";
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << '\n';
return 1;
}
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 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 = subprocess.check_output([str(binary)], text=True)
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.util.*;
import java.util.concurrent.*;
public class Euler311 {
static long isqrt(long n) {
long r = (long) Math.sqrt(n);
while ((r + 1) * (r + 1) <= n)
r++;
while (r * r > n)
r--;
return r;
}
static long isqrtCeil(long n) {
long r = isqrt(n);
return (r * r == n) ? r : r + 1;
}
static boolean isTwiceSquare(long n) {
if ((n & 1) != 0)
return false;
long m = n >> 1;
long r = isqrt(m);
return r * r == m;
}
static long nChoose2(long n) {
return (n < 2) ? 0 : (n * (n - 1)) / 2;
}
static long nChoose3(long n) {
return (n < 3) ? 0 : (n * (n - 1) * (n - 2)) / 6;
}
static class BlockTask implements Callable<Long> {
long low, high;
BlockTask(long low, long high) {
this.low = low;
this.high = high;
}
@Override
public Long call() {
int len = (int) (high - low + 1);
byte[] counts = new byte[len];
for (long y = 1;; ++y) {
long y2 = y * y;
long min_n = y2 + (y + 1) * (y + 1);
if (min_n > high)
break;
long xMin = y + 1;
long xMinN = y2 + xMin * xMin;
if (xMinN < low) {
xMin = isqrtCeil(low - y2);
if (xMin <= y)
xMin = y + 1;
}
long xMax = isqrt(high - y2);
if (xMax < xMin)
continue;
long x2 = xMin * xMin;
for (long x = xMin; x <= xMax; ++x) {
long n = y2 + x2;
int idx = (int) (n - low);
counts[idx]++;
x2 += 2 * x + 1;
}
}
long blockSum = 0;
for (int idx = 0; idx < len; ++idx) {
int count = counts[idx];
if (count >= 2) {
long n = low + idx;
blockSum += nChoose3(count);
if (isTwiceSquare(n)) {
blockSum += nChoose2(count);
}
}
}
return blockSum;
}
}
public static String solve() {
long limit = 10000000000L;
long maxT = limit / 4;
long blockSpan = 4000000;
int numThreads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<Long>> futures = new ArrayList<>();
long low = 1;
while (low <= maxT) {
long high = Math.min(maxT, low + blockSpan - 1);
futures.add(executor.submit(new BlockTask(low, high)));
low = high + 1;
}
long total = 0;
try {
for (Future<Long> f : futures) {
total += f.get();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
return String.valueOf(total);
}
public static void main(String[] args) {
System.out.println(solve());
}
}