Problem 210: Obtuse Angled Triangles
View on Project EulerProject Euler Problem 210 Solution
EulerSolve provides an optimized solution for Project Euler Problem 210, Obtuse Angled Triangles, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Let \(r\) be divisible by 4 and set \(a=r/4\). The fixed vertices are \(O=(0,0)\) and \(C=(a,a)\). The third vertex \(B=(x,y)\) ranges over all lattice points in the taxicab diamond $$|x|+|y|\le r.$$ The task is to count how many non-degenerate triangles \(OBC\) are obtuse. The geometric difficulty is that the allowed points form an \(L_1\)-ball rather than a Euclidean circle, while the obtuse-angle test itself is Euclidean. The implementations resolve this by splitting the count according to which vertex is obtuse and then converting the only nonlinear case into a lattice-point count inside a circle with a parity restriction. Mathematical Approach Separating the three obtuse-angle cases A triangle is obtuse exactly when one of its angles has negative dot product. With \(B=(x,y)\) and \(C=(a,a)\), the three angle tests are $$\overrightarrow{OB}\cdot\overrightarrow{OC}=a(x+y),$$ $$\overrightarrow{CO}\cdot\overrightarrow{CB}=2a^2-a(x+y),$$ $$\overrightarrow{BO}\cdot\overrightarrow{BC}=x^2+y^2-a(x+y).$$ Therefore the triangle is obtuse at $$O \iff x+y \lt 0,$$ $$C \iff x+y \gt 2a=\frac r2,$$ $$B \iff x^2+y^2-a(x+y) \lt 0.$$ These three regions can be counted separately, because a non-degenerate triangle cannot have more than one obtuse angle. The only overlap that matters is the collinear line \(y=x\), which will be removed at the end....
Detailed mathematical approach
Problem Summary
Let \(r\) be divisible by 4 and set \(a=r/4\). The fixed vertices are \(O=(0,0)\) and \(C=(a,a)\). The third vertex \(B=(x,y)\) ranges over all lattice points in the taxicab diamond
$$|x|+|y|\le r.$$
The task is to count how many non-degenerate triangles \(OBC\) are obtuse. The geometric difficulty is that the allowed points form an \(L_1\)-ball rather than a Euclidean circle, while the obtuse-angle test itself is Euclidean. The implementations resolve this by splitting the count according to which vertex is obtuse and then converting the only nonlinear case into a lattice-point count inside a circle with a parity restriction.
Mathematical Approach
Separating the three obtuse-angle cases
A triangle is obtuse exactly when one of its angles has negative dot product. With \(B=(x,y)\) and \(C=(a,a)\), the three angle tests are
$$\overrightarrow{OB}\cdot\overrightarrow{OC}=a(x+y),$$
$$\overrightarrow{CO}\cdot\overrightarrow{CB}=2a^2-a(x+y),$$
$$\overrightarrow{BO}\cdot\overrightarrow{BC}=x^2+y^2-a(x+y).$$
Therefore the triangle is obtuse at
$$O \iff x+y \lt 0,$$
$$C \iff x+y \gt 2a=\frac r2,$$
$$B \iff x^2+y^2-a(x+y) \lt 0.$$
These three regions can be counted separately, because a non-degenerate triangle cannot have more than one obtuse angle. The only overlap that matters is the collinear line \(y=x\), which will be removed at the end.
Counting the region where the angle at \(O\) is obtuse
The diamond \( |x|+|y|\le r \) contains
$$1+2r(r+1)=2r^2+2r+1$$
lattice points in total. The boundary line between \(x+y \lt 0\) and \(x+y \gt 0\) is \(x+y=0\), whose lattice points are \((t,-t)\) with \(|t|\le r/2\), so it contributes exactly \(r+1\) points.
By symmetry of the diamond across the line \(x+y=0\), the strict half-plane \(x+y \lt 0\) contains half of the remaining points:
$$N_O=\frac{(2r^2+2r+1)-(r+1)}{2}=r^2+\frac r2.$$
This is the first closed-form term used by the implementations.
Counting the region where the angle at \(C\) is obtuse
For the inequality \(x+y \gt r/2\), it is convenient to switch to diagonal coordinates
$$p=x+y,\qquad q=x-y.$$
Because \(x=(p+q)/2\) and \(y=(p-q)/2\), the lattice condition becomes \(p\equiv q\pmod 2\), and the diamond condition becomes
$$|p|\le r,\qquad |q|\le r.$$
Now fix a diagonal level \(p=s\) with \(r/2 \lt s\le r\). The allowed points on that level are exactly the integers \(q\in[-r,r]\) with the same parity as \(s\). When \(s\) is even there are \(r+1\) such values of \(q\); when \(s\) is odd there are \(r\).
Since \(r\) is divisible by 4, the levels \(s=r/2+1,r/2+2,\dots,r\) contain exactly \(r/4\) even values and \(r/4\) odd values. Hence
$$N_C=\frac r4(r+1)+\frac r4 r=\frac{r(2r+1)}{4}.$$
This is the second closed-form term.
Turning the angle at \(B\) into a circle count
The third condition is the only nonlinear one:
$$x^2+y^2-a(x+y) \lt 0.$$
Completing the square gives
$$\left(x-\frac a2\right)^2+\left(y-\frac a2\right)^2 \lt \frac{a^2}{2},$$
or, after multiplying by 4,
$$ (2x-a)^2+(2y-a)^2 \lt 2a^2. $$
So the points with an obtuse angle at \(B\) are exactly the lattice points strictly inside the circle whose diameter is \(OC\). The implementations use the scaled coordinates
$$u=2x-a,\qquad v=2y-a,$$
because then \(u\) and \(v\) are integers and both have the same parity as \(a\). Since the left-hand side is an integer, the strict inequality is equivalent to
$$u^2+v^2\le 2a^2-1.$$
Thus \(N_B\) is a lattice-circle count with a fixed parity pattern. The code scans admissible nonnegative \(u\)-values in steps of 2, keeps the largest same-parity \(v\) satisfying the circle inequality, and moves that \(v\)-pointer only downward. If the current maximum is \(v\), then the number of signed same-parity values from \(-v\) to \(v\) is \(v+1\), and the column contributes once when \(u=0\) and twice when \(u\ne 0\) because of the symmetry \(u\leftrightarrow -u\).
Worked example: \(r=8\)
Here \(a=2\). The three main pieces are
$$N_O=8^2+\frac 82=68,\qquad N_C=\frac{8(17)}{4}=34.$$
For the \(B\)-obtuse region, the inequality becomes
$$x^2+y^2-2(x+y)\lt 0\iff (x-1)^2+(y-1)^2\lt 2.$$
The integer points strictly inside that circle are
$$ (1,0),\ (0,1),\ (1,1),\ (2,1),\ (1,2), $$
so \(N_B=5\). One of them, namely \((1,1)\), lies on the line \(y=x\) and is degenerate. After the global correction discussed below, the total becomes
$$68+34+5-7=100,$$
which matches the small checkpoint used by the implementations.
Why the degenerate correction is \(r-1\)
All points \(B=(t,t)\) with \(-r/2\le t\le r/2\) are collinear with \(O\) and \(C\), so they do not form valid triangles. Two of these points, \(t=0\) and \(t=a\), are \(O\) and \(C\) themselves and were never counted, because the corresponding dot products are zero rather than negative.
Every other point on \(y=x\) was counted exactly once by the three cases above:
$$t\lt 0 \Rightarrow \text{counted in }N_O,$$
$$0\lt t\lt a \Rightarrow \text{counted in }N_B,$$
$$t\gt a \Rightarrow \text{counted in }N_C.$$
The number of such points is
$$\frac r2+\left(\frac r4-1\right)+\frac r4=r-1.$$
Therefore the final formula is
$$N(r)=N_O+N_C+N_B-(r-1).$$
How the Code Works
The C++, Python, and Java implementations first compute \(a=r/4\), then evaluate the two closed-form contributions \(N_O=r^2+r/2\) and \(N_C=r(2r+1)/4\) directly with integer arithmetic. No floating-point geometry is needed for those parts.
The only iterative part is \(N_B\). The implementation converts the problem to \(u^2+v^2\le 2a^2-1\) with \(u\equiv v\equiv a\pmod 2\). It finds the initial admissible \(v\) with an integer square root, iterates over admissible \(u\)-values in steps of 2, and shrinks \(v\) only when the current pair lies outside the circle. Because the maximum feasible \(v\) never increases as \(u\) increases, this is a monotone sweep rather than a two-dimensional search.
For each accepted \(u\), the implementation adds \(v+1\) same-parity values of \(v\) and doubles the contribution unless \(u=0\). After that it subtracts the universal degeneracy correction \(r-1\). The C++ implementation also includes small-radius checkpoint tests and can split the \(u\)-range across worker threads, while the Python and Java implementations perform the same mathematics serially.
Complexity Analysis
The two linear-half-plane counts and the final degeneracy correction are \(O(1)\). The circle phase examines only admissible \(u\)-values, so its outer loop has \(O(a)\) iterations, and the \(v\)-pointer moves downward at most \(O(a)\) times overall. Hence the total running time is \(O(a)=O(r)\), with \(a=r/4\).
The memory usage is \(O(1)\) for the serial implementations and still \(O(1)\) extra per worker in the threaded C++ variant. The key optimization is that the circle is never sampled point by point; instead, each admissible \(u\)-column is counted in one step after locating its topmost valid \(v\).
Footnotes and References
- Project Euler problem page: Problem 210 - Obtuse Angled Triangles
- Dot product and angle tests: Wikipedia - Dot product
- The circle with diameter \(OC\) and the obtuse-angle criterion: Wikipedia - Thales's theorem
- The diamond \( |x|+|y|\le r \) as a taxicab ball: Wikipedia - Taxicab geometry
- Lattice-point counting in circles: Wikipedia - Gauss circle problem
Problem 210 source code
C++
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
#include <thread>
#include <vector>
namespace {
using u64 = std::uint64_t;
using i64 = std::int64_t;
using i128 = __int128_t;
constexpr u64 kDefaultR = 1'000'000'000ULL;
constexpr u64 kCheckpointR1 = 4ULL;
constexpr u64 kCheckpointExpected1 = 24ULL;
constexpr u64 kCheckpointR2 = 8ULL;
constexpr u64 kCheckpointExpected2 = 100ULL;
constexpr u64 kThreadConsistencyR = 4'000'000ULL;
struct Options {
u64 r = kDefaultR;
bool allow_multithreading = true;
bool run_checkpoints = true;
unsigned requested_threads = 0;
};
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 = 0;
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 = 0;
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 r = 0;
if (parse_u64_after_prefix(arg, "--r=", r)) {
options.r = r;
continue;
}
unsigned threads = 0;
if (parse_unsigned_after_prefix(arg, "--threads=", threads)) {
options.requested_threads = threads;
continue;
}
std::cerr << "Unknown argument: " << arg << '\n';
return false;
}
if (options.r == 0ULL) {
std::cerr << "--r must be >= 1.\n";
return false;
}
if ((options.r % 4ULL) != 0ULL) {
std::cerr << "This solver requires r to be divisible by 4.\n";
return false;
}
return true;
}
u64 isqrt_u64(u64 n) {
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;
}
unsigned choose_thread_count(bool allow_multithreading,
unsigned requested_threads,
std::size_t workload) {
if (!allow_multithreading || workload < 2'000'000ULL) {
return 1;
}
unsigned threads = requested_threads;
if (threads == 0) {
threads = std::thread::hardware_concurrency();
if (threads == 0) {
threads = 1;
}
}
return std::max(1u, std::min<unsigned>(threads, static_cast<unsigned>(workload)));
}
i128 count_circle_u_range(u64 limit, int parity, u64 begin_u, u64 end_u) {
if (begin_u > end_u) {
return 0;
}
constexpr u64 kNoValue = std::numeric_limits<u64>::max();
u64 v = isqrt_u64(limit - begin_u * begin_u);
if (static_cast<int>(v & 1ULL) != parity) {
if (v == 0ULL) {
v = kNoValue;
} else {
--v;
}
}
i128 total = 0;
for (u64 u = begin_u; u <= end_u; u += 2ULL) {
if (v == kNoValue) {
break;
}
const u64 u2 = u * u;
while (u2 + v * v > limit) {
if (v <= 1ULL) {
v = kNoValue;
break;
}
v -= 2ULL;
}
if (v == kNoValue) {
break;
}
const i128 count_v = static_cast<i128>(v + 1ULL);
const i128 multiplicity = (u == 0ULL ? static_cast<i128>(1) : static_cast<i128>(2));
total += multiplicity * count_v;
}
return total;
}
u64 count_circle_points_with_parity(u64 a,
bool allow_multithreading,
unsigned requested_threads) {
const u64 limit = 2ULL * a * a - 1ULL;
const int parity = static_cast<int>(a & 1ULL);
u64 max_u = isqrt_u64(limit);
if (static_cast<int>(max_u & 1ULL) != parity) {
--max_u;
}
const u64 first_u = static_cast<u64>(parity);
if (first_u > max_u) {
return 0ULL;
}
const u64 step_count = (max_u - first_u) / 2ULL + 1ULL;
const unsigned threads =
choose_thread_count(allow_multithreading, requested_threads, static_cast<std::size_t>(step_count));
i128 total = 0;
if (threads == 1U) {
total = count_circle_u_range(limit, parity, first_u, max_u);
} else {
std::vector<std::thread> workers;
std::vector<i128> partial(threads, 0);
workers.reserve(threads);
for (unsigned t = 0; t < threads; ++t) {
const u64 step_begin = static_cast<u64>((static_cast<__int128>(step_count) * t) / threads);
const u64 step_end_exclusive =
static_cast<u64>((static_cast<__int128>(step_count) * (t + 1ULL)) / threads);
if (step_begin >= step_end_exclusive) {
continue;
}
const u64 begin_u = first_u + 2ULL * step_begin;
const u64 end_u = first_u + 2ULL * (step_end_exclusive - 1ULL);
workers.emplace_back([&, t, begin_u, end_u]() {
partial[t] = count_circle_u_range(limit, parity, begin_u, end_u);
});
}
for (std::thread& worker : workers) {
worker.join();
}
for (const i128 value : partial) {
total += value;
}
}
return static_cast<u64>(total);
}
i128 solve_obtuse_count(u64 r,
bool allow_multithreading,
unsigned requested_threads) {
const u64 a = r / 4ULL;
// Angle at O is obtuse when x + y < 0.
const i128 count_o = static_cast<i128>(r) * static_cast<i128>(r) +
static_cast<i128>(r / 2ULL);
// Angle at C is obtuse when x + y > r/2.
const i128 count_c =
static_cast<i128>(r) * static_cast<i128>(2ULL * r + 1ULL) / static_cast<i128>(4);
// Angle at B is obtuse when B is strictly inside the circle with diameter OC.
const i128 count_b = static_cast<i128>(count_circle_points_with_parity(
a,
allow_multithreading,
requested_threads));
// Degenerate collinear points on y = x were included once above but must be excluded.
const i128 degenerate = static_cast<i128>(r - 1ULL);
return count_o + count_c + count_b - degenerate;
}
i128 brute_force_count(u64 r) {
const i64 a = static_cast<i64>(r / 4ULL);
i128 count = 0;
for (i64 x = -static_cast<i64>(r); x <= static_cast<i64>(r); ++x) {
for (i64 y = -static_cast<i64>(r); y <= static_cast<i64>(r); ++y) {
if (std::llabs(x) + std::llabs(y) > static_cast<i64>(r)) {
continue;
}
if (x == y) {
// O, B, C are collinear -> largest angle is 180 degrees, excluded.
continue;
}
const i64 sum = x + y;
const i64 dot_o = a * sum;
const i64 dot_b = x * x + y * y - a * sum;
const i64 dot_c = 2LL * a * a - a * sum;
if (dot_o < 0LL || dot_b < 0LL || dot_c < 0LL) {
++count;
}
}
}
return count;
}
std::string to_string_i128(i128 value) {
if (value == 0) {
return "0";
}
bool negative = value < 0;
if (negative) {
value = -value;
}
std::string out;
while (value > 0) {
const int digit = static_cast<int>(value % 10);
out.push_back(static_cast<char>('0' + digit));
value /= 10;
}
if (negative) {
out.push_back('-');
}
std::reverse(out.begin(), out.end());
return out;
}
bool run_checkpoints(const Options& options) {
struct FixedCheckpoint {
u64 r;
u64 expected;
};
const std::vector<FixedCheckpoint> fixed = {
{kCheckpointR1, kCheckpointExpected1},
{kCheckpointR2, kCheckpointExpected2},
};
for (const FixedCheckpoint& cp : fixed) {
const i128 got = solve_obtuse_count(cp.r, false, 1U);
if (got != static_cast<i128>(cp.expected)) {
std::cerr << "Checkpoint failed: N(" << cp.r << ") expected " << cp.expected
<< ", got " << to_string_i128(got) << '\n';
return false;
}
std::cout << "Checkpoint OK: N(" << cp.r << ") = " << cp.expected << '\n';
}
const std::vector<u64> brute_cases = {12ULL, 16ULL, 20ULL, 24ULL, 28ULL, 32ULL};
for (const u64 r : brute_cases) {
const i128 brute = brute_force_count(r);
const i128 fast = solve_obtuse_count(r, false, 1U);
if (brute != fast) {
std::cerr << "Brute checkpoint failed: N(" << r << ") brute=" << to_string_i128(brute)
<< ", fast=" << to_string_i128(fast) << '\n';
return false;
}
std::cout << "Checkpoint OK: brute cross-check N(" << r << ") = "
<< to_string_i128(fast) << '\n';
}
if (options.allow_multithreading) {
const i128 single_thread = solve_obtuse_count(kThreadConsistencyR, false, 1U);
const i128 multi_thread =
solve_obtuse_count(kThreadConsistencyR, true, options.requested_threads);
if (single_thread != multi_thread) {
std::cerr << "Thread-consistency checkpoint failed at N(" << kThreadConsistencyR
<< "): single=" << to_string_i128(single_thread)
<< ", multi=" << to_string_i128(multi_thread) << '\n';
return false;
}
std::cout << "Checkpoint OK: threaded consistency at N(" << kThreadConsistencyR
<< ") = " << to_string_i128(single_thread) << '\n';
}
return true;
}
} // namespace
int main(int argc, char** argv) {
Options options;
if (!parse_arguments(argc, argv, options)) {
return 1;
}
const auto start = std::chrono::steady_clock::now();
if (options.run_checkpoints) {
if (!run_checkpoints(options)) {
return 1;
}
}
const i128 answer =
solve_obtuse_count(options.r, options.allow_multithreading, options.requested_threads);
const auto finish = std::chrono::steady_clock::now();
const std::chrono::duration<long double> elapsed = finish - start;
std::cout << "Answer: " << to_string_i128(answer) << '\n';
std::cout << "N(" << options.r << ") = " << to_string_i128(answer) << '\n';
std::cout << "Elapsed: " << elapsed.count() << " s\n";
return 0;
}
Python
import math
def solve():
R = 1_000_000_000
def isqrt_u64(n):
r = math.isqrt(n)
return r
def count_circle_u_range(limit, parity, begin_u, end_u):
if begin_u > end_u:
return 0
v = isqrt_u64(limit - begin_u * begin_u)
if (v & 1) != parity:
if v == 0:
return 0
v -= 1
total = 0
u = begin_u
while u <= end_u:
u2 = u * u
while u2 + v * v > limit:
if v <= 1:
v = -1
break
v -= 2
if v < 0:
break
count_v = v + 1
multiplicity = 1 if u == 0 else 2
total += multiplicity * count_v
u += 2
return total
a = R // 4
limit = 2 * a * a - 1
parity = a & 1
max_u = isqrt_u64(limit)
if (max_u & 1) != parity:
max_u -= 1
first_u = parity
count_b = count_circle_u_range(limit, parity, first_u, max_u)
count_o = R * R + R // 2
count_c = R * (2 * R + 1) // 4
degenerate = R - 1
answer = count_o + count_c + count_b - degenerate
return str(answer)
if __name__ == '__main__':
print(solve())
Java
public class Euler210 {
public static void main(String[] args) {
long r = 1000000000L;
long a = r / 4;
long countO = r * r + r / 2;
long countC = r * (2 * r + 1) / 4;
// count_b: circle points with parity
long limit = 2 * a * a - 1;
int parity = (int) (a & 1);
long maxU = isqrt(limit);
if ((maxU & 1) != parity)
maxU--;
long firstU = parity;
long totalB = 0;
long v = isqrt(limit - firstU * firstU);
if ((v & 1) != parity) {
v--;
if (v < 0)
v = -1;
}
for (long u = firstU; u <= maxU; u += 2) {
if (v < 0)
break;
long u2 = u * u;
while (u2 + v * v > limit) {
v -= 2;
if (v < 0)
break;
}
if (v < 0)
break;
long countV = v + 1;
long mult = (u == 0) ? 1 : 2;
totalB += mult * countV;
}
long degenerate = r - 1;
System.out.println(countO + countC + totalB - degenerate);
}
static long isqrt(long n) {
if (n < 0)
return 0;
long s = (long) Math.sqrt((double) n);
while (s * s > n)
s--;
while ((s + 1) * (s + 1) <= n)
s++;
return s;
}
}