Line Intersection
Representations
| Form | Good for |
|---|---|
| Two points | input format, cross product tests |
| Point + direction | parametric queries, sweeps |
| solving systems, half-planes |
Converting from two points to the implicit form:
This is : the implicit form’s normal is the direction rotated .
Intersecting two lines
Solve by Cramer’s rule with :
bool lineIntersect(double A1, double B1, double C1, double A2, double B2, double C2, PD& out) {
double D = A1 * B2 - A2 * B1;
if (fabs(D) < 1e-9) return false; // parallel or identical
out.x = (C1 * B2 - C2 * B1) / D;
out.y = (A1 * C2 - A2 * C1) / D;
return true;
}equals the cross product of the two direction vectors, so means parallel.
The vector form — usually cleaner
Line 1 through with direction ; line 2 through with direction :
bool lineInter(P a, P b, P c, P d, PD& out) {
P u = b - a, v = d - c;
ll den = cross(u, v);
if (den == 0) return false; // parallel
double t = (double)cross(c - a, v) / den;
out = {a.x + u.x * t, a.y + u.y * t};
return true;
}den is exact in integers, so the parallel test is exact even though the intersection point is floating point. That separation — exact predicate, approximate construction — is the right pattern throughout geometry.
Proof: Cramer's rule, and
Cramer’s rule solves by , with the matrix determinant — direct substitution verifies both equations when . The vector-form derivation: at the intersection, for some , so ; taking of both sides eliminates (since ), giving , i.e. the formula above.
For : writing for each line, (the two minus signs cancel in a product of two cross-product arguments… more carefully, ). Since , — rotating both vectors by the same preserves their cross product. Hence .
Parallel vs identical
// cross(u,v) == 0: parallel or identical.
// cross(u, c - a) == 0 too: c lies on line ab, so the lines are IDENTICAL; otherwise PARALLEL (disjoint).Segment intersection is a different problem
Two lines almost always meet; two segments usually do not. After computing (and the corresponding on the other segment), require — see below for the full case analysis including collinear overlap.
Related constructions
PD foot(P p, P a, P b) { // the perpendicular foot from p onto line ab
double t = (double)dot(p - a, b - a) / norm2(b - a);
return toPD(a) + toPD(b - a) * t;
}
PD midpoint(P a, P b) { return (toPD(a) + toPD(b)) * 0.5; }
P bisectorDir(P a, P b) { return perp(b - a); } // direction of the perpendicular bisector of ab| Construction | Method |
|---|---|
| Circumcentre of a triangle | intersect two perpendicular bisectors (or the closed form) |
| Incentre | intersect two angle bisectors, or the weighted average |
| Orthocentre | intersect two altitudes (or ) |
| Centroid | |
| Reflection of across line | reflectAcross(p,a,b) from Primitives, |
| Angle bisector direction at | normalise and , then add |
Numerical care
Nearly parallel lines
When is tiny but non-zero, the intersection point is enormous and its relative error is huge. If the algorithm then feeds that point into further computations, the error compounds badly. Mitigations: keep the exact
crossfor all decisions and only construct the point when it is genuinely needed; scale coordinates so they are comparable in magnitude; uselong double.
Lines through many points
Detecting collinear triples among points: for each point, sort the others by direction around it and group equal directions — . Canonicalise each direction by dividing by the gcd and fixing the sign, so (2,4) and (-1,-2) compare equal:
pair<ll,ll> canonicalDir(P d) {
ll g = __gcd(llabs(d.x), llabs(d.y));
if (g) { d.x /= g; d.y /= g; }
if (d.x < 0 || (d.x == 0 && d.y < 0)) { d.x = -d.x; d.y = -d.y; }
return {d.x, d.y};
}Segment Intersection
Do segments and intersect? The exact-integer answer uses the four orientation tests of properIntersect, plus a collinear case.
bool segmentsIntersect(P a, P b, P c, P d) {
int o1 = orient(a, b, c), o2 = orient(a, b, d);
int o3 = orient(c, d, a), o4 = orient(c, d, b);
if (o1 != o2 && o3 != o4) return true; // proper crossing
if (o1 == 0 && onSegment(c, a, b)) return true; // collinear / touching cases
if (o2 == 0 && onSegment(d, a, b)) return true;
if (o3 == 0 && onSegment(a, c, d)) return true;
if (o4 == 0 && onSegment(b, c, d)) return true;
return false;
}(onSegment, orient: Primitives.) Everything here is exact integer arithmetic.
The degenerate cases
| Case | Handled by |
|---|---|
| Segments cross properly | o1 != o2 && o3 != o4 |
| An endpoint lies on the other segment | the collinear checks |
| Segments are collinear and overlap | the collinear checks |
| Segments are collinear and disjoint | all checks fail — correct |
| A segment is a single point | onSegment still works |
| Segments share exactly one endpoint | reported as intersecting |
Decide what "touching" means
Many problems distinguish “properly cross” from “merely touch at an endpoint”. Read the statement and use
properIntersect(the first condition only, from Primitives) when touching should not count.
Proof:
segmentsIntersectis correctThe strict-crossing case
o1≠o2 && o3≠o4is `properIntersect`, proved there. It remains to handle the case where at least one , i.e. one segment has an endpoint collinear with the other segment’s line — ruling out a transversal crossing but not a touch or overlap.If : is collinear with line . The two segments meet somewhere iff that meeting point is itself (since is on line , the only way segment can touch segment through its collinear endpoint is if itself lies on segment — any other point of is off the line, hence off segment too, unless is also collinear with , which is exactly the case where and the check for is symmetric with the check for ). So the test is precisely
onSegment(c,a,b). Symmetric reasoning covers (endpoint ), (endpoint on line ) and (endpoint ).These four checks cover every way the two segments can meet without a strict crossing: a shared or absorbed endpoint, or collinear overlap (where at least one endpoint of one segment lies within the other, which is guaranteed whenever two collinear segments overlap at all — if neither endpoint of lay in and neither endpoint of lay in , the two collinear segments would be disjoint). Conversely, if any check succeeds, that point is a shared point of both segments by
onSegment’s own correctness.
The intersection point
// assumes they intersect and are not parallel
PD intersectionPoint(P a, P b, P c, P d) {
P u = b - a, v = d - c;
double t = (double)cross(c - a, v) / cross(u, v);
return toPD(a) + toPD(u) * t;
}This is exactly lineInter above. For the collinear overlap case there is no single point — the intersection is a sub-segment; compute it by projecting all four endpoints onto the common direction and taking the overlapping interval of parameters.
Exact rational intersection
When the point must be exact (to compare with others, or to deduplicate), keep the same as a fraction instead of evaluating it:
storing numerator and denominator separately and reducing by their gcd. Avoids all floating-point comparison problems, at the cost of __int128 (the numerator is a product of three terms) and some care.
Any-intersection among segments
| Question | Method | Time |
|---|---|---|
| Do any two of segments intersect? | Shamos-Hoey sweep | |
| Report all intersections | Bentley-Ottmann | |
| Count intersections only | sweep + BIT for special cases (axis-parallel) | |
| Brute force | all pairs |
For , the loop is 4 million exact integer tests — fast, simple, and unhackable. Reach for a sweep only when is large. The general sweeps are covered in Sweep Line.
Axis-parallel special case
When all segments are horizontal or vertical, counting intersections is a classic sweep + Fenwick problem:
- Sweep from left to right.
- A horizontal segment starting activate its in the Fenwick; ending deactivate.
- A vertical segment at this spanning query how many active lie in that range.
, exact, and much simpler than the general Bentley-Ottmann.
Proof: the axis-parallel sweep counts correctly
A horizontal segment and vertical segment cross iff ‘s -coordinate lies in ‘s -range and ‘s -coordinate lies in ‘s -range. Processing events left to right by (with activations before deactivations at equal , or exactly matching the half-open convention the problem wants for touching endpoints), is “active” (its is in the Fenwick) exactly while the sweep is inside ‘s -range. So when the sweep reaches each vertical segment , querying the Fenwick for active ‘s in ‘s -range counts exactly the horizontal segments currently spanning ‘s with in range — precisely the horizontals crossing . Summing over all verticals counts every crossing pair once.
Ray-segment and ray-polygon
For point-in-polygon by ray casting, you need “does a horizontal ray from cross this edge”. The standard robust form avoids computing intersection points entirely:
bool crosses(P p, P a, P b) {
if (a.y > b.y) swap(a, b);
if (p.y <= a.y || p.y > b.y) return false; // half-open: counts each vertex once
return orient(a, b, p) > 0;
}Proof: the half-open -range is necessary and sufficient
The ray is (rightward from ). An edge with after the swap can only meet this ray’s height if ; the function instead uses (half-open on the low end, inclusive on the high end). This specific choice makes every lattice -value that is exactly a vertex’s -coordinate belong to exactly one of the two edges meeting at that vertex (the edge going up includes it as its top, the edge going down excludes it as its bottom, or vice versa under the swap normalization) — so a ray passing exactly through a vertex is counted once, not zero or two times, regardless of which of the two incident edges “owns” that height. Any other convention (both inclusive, both exclusive, or the complementary half-open choice) either double-counts or misses rays through vertices. Given the height is in range,
orient(a,b,p) > 0(with below ) tests that is to the right of the edge, i.e. that the ray going right from actually meets the edge rather than passing behind it — exactly the cross product sign for ” is clockwise of (below-right of) the upward edge ”.