Problem 210: Obtuse Angled Triangles

View on Project Euler

Project 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

  1. Project Euler problem page: Problem 210 - Obtuse Angled Triangles
  2. Dot product and angle tests: Wikipedia - Dot product
  3. The circle with diameter \(OC\) and the obtuse-angle criterion: Wikipedia - Thales's theorem
  4. The diamond \( |x|+|y|\le r \) as a taxicab ball: Wikipedia - Taxicab geometry
  5. 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;
    }
}