Problem 102: Triangle Containment

View on Project Euler

Project Euler Problem 102 Solution

EulerSolve provides an optimized solution for Project Euler Problem 102, Triangle Containment, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Each input row gives one triangle \(ABC\) by listing three integer vertices. The task is to count how many of those triangles contain the origin \(O=(0,0)\) in their interior. Because the tested point never changes, this is not a general point-in-polygon problem; the geometry collapses to a small orientation test involving three signed areas. Mathematical Approach Let \(A=(x_1,y_1)\), \(B=(x_2,y_2)\), and \(C=(x_3,y_3)\). The implementations do not use floating-point angles, explicit line intersections, or any iterative search. They evaluate three determinants formed with the origin: $$s_1=x_1y_2-y_1x_2,\qquad s_2=x_2y_3-y_2x_3,\qquad s_3=x_3y_1-y_3x_1.$$ Geometrically, \(s_1\), \(s_2\), and \(s_3\) are twice the signed areas of the triangles \(OAB\), \(OBC\), and \(OCA\). Their signs tell us whether each consecutive pair of vertex vectors turns counterclockwise or clockwise around the origin. The Signed-Area Identity The whole triangle also has a signed area, namely $$\Delta=\det(B-A,C-A)=x_1(y_2-y_3)+x_2(y_3-y_1)+x_3(y_1-y_2).$$ Expanding this determinant gives the key identity $$\Delta=s_1+s_2+s_3.$$ So the three subareas cut out by the origin add up to the oriented area of \(ABC\). This is the invariant behind the containment test: the origin is inside exactly when those subareas fit together with a consistent orientation....

Detailed mathematical approach

Problem Summary

Each input row gives one triangle \(ABC\) by listing three integer vertices. The task is to count how many of those triangles contain the origin \(O=(0,0)\) in their interior. Because the tested point never changes, this is not a general point-in-polygon problem; the geometry collapses to a small orientation test involving three signed areas.

Mathematical Approach

Let \(A=(x_1,y_1)\), \(B=(x_2,y_2)\), and \(C=(x_3,y_3)\). The implementations do not use floating-point angles, explicit line intersections, or any iterative search. They evaluate three determinants formed with the origin:

$$s_1=x_1y_2-y_1x_2,\qquad s_2=x_2y_3-y_2x_3,\qquad s_3=x_3y_1-y_3x_1.$$

Geometrically, \(s_1\), \(s_2\), and \(s_3\) are twice the signed areas of the triangles \(OAB\), \(OBC\), and \(OCA\). Their signs tell us whether each consecutive pair of vertex vectors turns counterclockwise or clockwise around the origin.

The Signed-Area Identity

The whole triangle also has a signed area, namely

$$\Delta=\det(B-A,C-A)=x_1(y_2-y_3)+x_2(y_3-y_1)+x_3(y_1-y_2).$$

Expanding this determinant gives the key identity

$$\Delta=s_1+s_2+s_3.$$

So the three subareas cut out by the origin add up to the oriented area of \(ABC\). This is the invariant behind the containment test: the origin is inside exactly when those subareas fit together with a consistent orientation.

Barycentric Coordinates of the Origin

A point lies strictly inside a triangle exactly when its barycentric coordinates are all positive. For the origin, the representation

$$O=\lambda_1A+\lambda_2B+\lambda_3C,\qquad \lambda_1+\lambda_2+\lambda_3=1$$

has the especially simple closed form

$$\lambda_1=\frac{s_2}{\Delta},\qquad \lambda_2=\frac{s_3}{\Delta},\qquad \lambda_3=\frac{s_1}{\Delta}.$$

Therefore \(O\) is strictly inside \(ABC\) if and only if all three ratios are positive. Since the denominator is common, this happens precisely when \(s_1\), \(s_2\), and \(s_3\) all have the same nonzero sign.

Why the Determinant Test Is Enough

A triangle is convex, so one negative barycentric coordinate already proves that the point lies outside the edge opposite the corresponding vertex. In determinant language, if one of \(s_1,s_2,s_3\) has the opposite sign from the other two, the origin is outside. If one determinant is zero, then \(O\) is collinear with one side, so the origin lies on the boundary rather than in the strict interior.

That is why the decisive condition is simply

$$s_1,s_2,s_3 \gt 0\quad\text{or}\quad s_1,s_2,s_3 \lt 0.$$

No divisions are needed in the implementation, and integer arithmetic keeps the test exact.

Worked Example

Consider the triangle

$$A=(-340,495),\qquad B=(-153,-910),\qquad C=(835,-947).$$

Then

$$s_1=385135,\qquad s_2=904741,\qquad s_3=91345.$$

All three values are positive, so all three barycentric coordinates of the origin are positive as well. Hence the origin lies inside this triangle.

For comparison, with

$$A=(-175,41),\qquad B=(-421,-714),\qquad C=(574,-645)$$

we get

$$s_1=142211,\qquad s_2=681381,\qquad s_3=-89341.$$

The signs are mixed, so one barycentric coordinate is negative and the origin lies outside.

How the Code Works

The C++, Python, and Java implementations all scan the triangle list row by row, split each row into six integers, and interpret them as the coordinates of \(A\), \(B\), and \(C\). For each triangle they compute the three determinants \(s_1\), \(s_2\), and \(s_3\) above. There is no preprocessing phase because every triangle can be decided independently.

The final decision is a sign check. The C++ implementation uses the strict form "all three positive or all three negative," which matches strict containment exactly. The Python and Java implementations encode the equivalent "no mixed signs" rule; away from the boundary case \(s_i=0\), it gives the same answer. Every triangle that passes the test increments a running counter, and that counter is the final output.

Complexity Analysis

If there are \(N\) triangles, the algorithm performs a constant amount of work per triangle: three 2-by-2 determinants and a few sign comparisons. The total running time is therefore \(O(N)\).

The geometric test itself uses \(O(1)\) extra space. Some implementations may store input lines before processing them, but the mathematical core needs only the current triangle and the running count. For the Project Euler input size, the computation is effectively instantaneous.

Footnotes and References

  1. Problem page: Project Euler 102
  2. Cross product and planar orientation: Wikipedia - Cross product
  3. Barycentric coordinates: Wikipedia - Barycentric coordinate system
  4. Coordinate formulas for triangle area: Wikipedia - Area of a triangle
  5. Containment tests based on orientation: Wikipedia - Point in polygon

Problem 102 source code

C++

#include <cstdint>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

using i64 = std::int64_t;

struct Options {
    std::string file = "resources/documents/0102_triangles.txt";
    bool run_checkpoints = true;
};

struct Point {
    i64 x = 0;
    i64 y = 0;
};

bool parse_string_after_prefix(const std::string& arg,
                               const std::string& prefix,
                               std::string& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    value = tail;
    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 == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_string_after_prefix(arg, "--file=", options.file)) {
            continue;
        }

        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }

    return true;
}

i64 cross(const Point& a, const Point& b) {
    return a.x * b.y - a.y * b.x;
}

bool contains_origin(const Point& a, const Point& b, const Point& c) {
    const i64 c1 = cross(a, b);
    const i64 c2 = cross(b, c);
    const i64 c3 = cross(c, a);

    const bool all_positive = (c1 > 0 && c2 > 0 && c3 > 0);
    const bool all_negative = (c1 < 0 && c2 < 0 && c3 < 0);
    return all_positive || all_negative;
}

std::vector<i64> parse_csv_i64(const std::string& line) {
    std::vector<i64> values;
    std::istringstream input(line);
    std::string token;
    while (std::getline(input, token, ',')) {
        values.push_back(std::stoll(token));
    }
    return values;
}

int solve_from_text(const std::string& text) {
    std::istringstream input(text);
    std::string line;
    int count = 0;

    while (std::getline(input, line)) {
        if (line.empty()) {
            continue;
        }
        const std::vector<i64> v = parse_csv_i64(line);
        if (v.size() != 6) {
            throw std::runtime_error("Malformed triangle row");
        }

        const Point a{v[0], v[1]};
        const Point b{v[2], v[3]};
        const Point c{v[4], v[5]};
        if (contains_origin(a, b, c)) {
            ++count;
        }
    }

    return count;
}

int solve(const std::string& file_path) {
    std::ifstream input(file_path);
    if (!input) {
        throw std::runtime_error("Could not open triangles file: " + file_path);
    }

    std::ostringstream buffer;
    buffer << input.rdbuf();
    return solve_from_text(buffer.str());
}

bool run_checkpoints() {
    const std::string sample =
        "-340,495,-153,-910,835,-947\n"
        "-175,41,-421,-714,574,-645\n";

    if (solve_from_text(sample) != 1) {
        std::cerr << "Checkpoint failed for statement sample" << '\n';
        return false;
    }

    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }
    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }

    try {
        std::cout << solve(options.file) << '\n';
    } catch (const std::exception& ex) {
        std::cerr << ex.what() << '\n';
        return 3;
    }

    return 0;
}

Python

# Problem 102: Triangle containment
# How many triangles in the file contain the origin?

import os

def solve():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    file_path = os.path.join(script_dir, '..', 'resources', 'documents', '0102_triangles.txt')
    
    def contains_origin(x1, y1, x2, y2, x3, y3):
        # Cross product method
        d1 = x1 * (y2 - y3) - y1 * (x2 - x3) + (x2 * y3 - x3 * y2)
        d2 = 0  # sub-triangles with origin
        # Check using sign of cross products
        def sign(x):
            return (x > 0) - (x < 0)
        def cross(ax, ay, bx, by):
            return ax * by - ay * bx
        
        c1 = cross(x1, y1, x2, y2)
        c2 = cross(x2, y2, x3, y3)
        c3 = cross(x3, y3, x1, y1)
        
        has_neg = (c1 < 0) or (c2 < 0) or (c3 < 0)
        has_pos = (c1 > 0) or (c2 > 0) or (c3 > 0)
        return not (has_neg and has_pos)
    
    with open(file_path) as f:
        lines = f.readlines()
    
    count = 0
    for line in lines:
        coords = list(map(int, line.strip().split(',')))
        if contains_origin(*coords):
            count += 1
    print(count)

solve()

Java

import java.nio.file.*;
import java.util.*;

public class Euler102 {
    public static void main(String[] args) throws Exception {
        List<String> lines = Files.readAllLines(Path.of("resources/documents/0102_triangles.txt"));
        int count = 0;
        for (String line : lines) {
            String[] p = line.trim().split(",");
            if (p.length < 6)
                continue;
            int x1 = Integer.parseInt(p[0]), y1 = Integer.parseInt(p[1]), x2 = Integer.parseInt(p[2]),
                    y2 = Integer.parseInt(p[3]), x3 = Integer.parseInt(p[4]), y3 = Integer.parseInt(p[5]);
            long c1 = (long) x1 * y2 - (long) y1 * x2, c2 = (long) x2 * y3 - (long) y2 * x3,
                    c3 = (long) x3 * y1 - (long) y3 * x1;
            boolean neg = c1 < 0 || c2 < 0 || c3 < 0, pos = c1 > 0 || c2 > 0 || c3 > 0;
            if (!(neg && pos))
                count++;
        }
        System.out.println(count);
    }
}