◐ Off-By-One · answer catalog

node-sweep-line-polygon-union-exact-rational-area

2 answer(s)jsnode20jsnode20

Given N closed polygons whose vertices are integer coordinates (orientation arbitrary, self-overlap allowed, so the union may contain holes), compute:

📦 Source in repository (JSON)

Answer 1

The solution is complete and verified. Here is the deliverable (also written to ~/SOLUTION.md, with runnable code at ~/solution.js).


Exact Union Area of Lattice Polygons via an Exact-Rational Vertical Slab Sweep

Problem

Given N closed polygons whose vertices are integer coordinates (orientation arbitrary, self-overlap allowed, so the union may contain holes), compute:

  1. the exact area of their union as a reduced fraction num/den (an integer when den == 1);
  2. the number of distinct intersection points among all polygon edges, including endpoints shared by two segments.

Floating-point accumulators are not acceptable: near-degenerate slivers are cancelled differently than the exact reference.

Root-cause analysis

The usual ways to attack this problem fail in predictable ways:

The fix is to do everything with exact reduced BigInt fractions and to use a sweep whose cell decomposition is guaranteed to contain no crossings.

Exact fix

Algorithm

  1. Rational arithmetic. Represent every value as [num, den] with den > 0, reduced by gcd. All comparisons are cross-multiplications.
  2. Critical values. Collect the x of every vertex and of every pairwise segment intersection. Sort/deduplicate them exactly.
  3. Slabs. For each consecutive pair of critical x values x0 < x1:
  4. no vertex and no crossing lies strictly inside (x0, x1);
  5. therefore the active edge set and their vertical order are constant on the slab, and the covered length L(x) is a linear function of x;
  6. the midpoint rule is exact for a linear function, so area_slab = (x1 - x0) * L((x0+x1)/2).
  7. Covered length at the midpoint. For each polygon, take its active edges (segments with xlo < mid < xhi), sort them by y(mid), and pair them with the even-odd rule to get inside intervals. Take the union of the intervals of all polygons; its total length is L(mid).
  8. Intersections. For every pair of edges compute all intersection points (proper crossings, endpoint touches, T-junctions, collinear overlap endpoints). Canonicalize each rational point to x/y in lowest terms and deduplicate with a Set. This is the required count.

Because every operation is exact, the final area is identical to the reference, including on near-degenerate slivers.

Full source (solution.js)

'use strict';

/*
 * Exact area of union of N simple (possibly self-intersecting) polygons
 * with integer vertices, plus the number of distinct segment intersection
 * points (including endpoints shared by two segments).
 *
 * Exact rational arithmetic with BigInt.  A Bentley-Ottmann style vertical
 * sweep is used: all critical x values (vertices + all pairwise segment
 * intersections) split the plane into vertical slabs.  Inside a slab the
 * set and the vertical order of active edges never change, so the covered
 * length L(x) is a linear function of x and the area of the slab is exactly
 * width * L(midpoint).
 */

/* ------------------------------------------------------------------ */
/* Exact non-negative rational arithmetic, [num, den] with den > 0,    */
/* always kept reduced.                                                */
/* ------------------------------------------------------------------ */

function babs(a) { return a < 0n ? -a : a; }

function bgcd(a, b) {
  a = babs(a); b = babs(b);
  while (b) { const t = a % b; a = b; b = t; }
  return a;
}

function R(n, d = 1n) {
  if (d === 0n) throw new Error('zero denominator');
  if (d < 0n) { n = -n; d = -d; }
  if (n === 0n) return [0n, 1n];
  const g = bgcd(n, d);
  return [n / g, d / g];
}

const radd = (a, b) => R(a[0] * b[1] + b[0] * a[1], a[1] * b[1]);
const rsub = (a, b) => R(a[0] * b[1] - b[0] * a[1], a[1] * b[1]);
const rmul = (a, b) => R(a[0] * b[0], a[1] * b[1]);
const rdiv = (a, b) => {
  if (b[0] === 0n) throw new Error('division by zero rational');
  return R(a[0] * b[1], a[1] * b[0]);
};
const rcmp = (a, b) => {
  const l = a[0] * b[1], r = b[0] * a[1];
  return l < r ? -1 : (l > r ? 1 : 0);
};
const rkey = (a) => a[0] + '/' + a[1];
const rInt = (v) => [BigInt(v), 1n];

/* ------------------------------------------------------------------ */
/* Integer geometry helpers.                                           */
/* ------------------------------------------------------------------ */

const cross = (o, a, b) =>
  (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);

function onSeg(a, b, p) {
  return cross(a, b, p) === 0n &&
    p[0] >= (a[0] < b[0] ? a[0] : b[0]) &&
    p[0] <= (a[0] > b[0] ? a[0] : b[0]) &&
    p[1] >= (a[1] < b[1] ? a[1] : b[1]) &&
    p[1] <= (a[1] > b[1] ? a[1] : b[1]);
}

/* Proper crossing point of two non-parallel lines AB and CD, rational. */
function lineIntersection(A, B, C, D) {
  const rx = B[0] - A[0], ry = B[1] - A[1];
  const sx = D[0] - C[0], sy = D[1] - C[1];
  const den = rx * sy - ry * sx;          // cross(r, s), non-zero here
  const cax = C[0] - A[0], cay = C[1] - A[1];
  const tNum = cax * sy - cay * sx;       // cross(ca, s)
  const t = R(tNum, den);
  return [
    radd(rInt(A[0]), rmul(t, rInt(rx))),
    radd(rInt(A[1]), rmul(t, rInt(ry))),
  ];
}

/*
 * All distinct intersection points of segments AB and CD, each returned as
 * a rational point [xRat, yRat].  For a collinear overlap the overlap
 * endpoints are returned.
 */
function segIntersections(A, B, C, D, out) {
  const d1 = cross(A, B, C);
  const d2 = cross(A, B, D);
  const d3 = cross(C, D, A);
  const d4 = cross(C, D, B);

  const pos1 = d1 > 0n, neg1 = d1 < 0n;
  const pos2 = d2 > 0n, neg2 = d2 < 0n;
  const pos3 = d3 > 0n, neg3 = d3 < 0n;
  const pos4 = d4 > 0n, neg4 = d4 < 0n;

  if ((pos1 && neg2 || neg1 && pos2) && (pos3 && neg4 || neg3 && pos4)) {
    out.push(lineIntersection(A, B, C, D));
    return;
  }

  if (d1 === 0n && d2 === 0n && d3 === 0n && d4 === 0n) {
    // Collinear: the overlap endpoints are exactly the endpoints that lie
    // on both segments.
    for (const p of [A, B, C, D]) {
      if (onSeg(A, B, p) && onSeg(C, D, p)) out.push([rInt(p[0]), rInt(p[1])]);
    }
    return;
  }

  if (d1 === 0n && onSeg(A, B, C)) out.push([rInt(C[0]), rInt(C[1])]);
  if (d2 === 0n && onSeg(A, B, D)) out.push([rInt(D[0]), rInt(D[1])]);
  if (d3 === 0n && onSeg(C, D, A)) out.push([rInt(A[0]), rInt(A[1])]);
  if (d4 === 0n && onSeg(C, D, B)) out.push([rInt(B[0]), rInt(B[1])]);
}

/* ------------------------------------------------------------------ */
/* Core solver.                                                        */
/* ------------------------------------------------------------------ */

function solve(polygons) {
  // Build the segment list; remember the owning polygon.
  const segs = [];              // { a:[bx,by], b:[bx,by], poly }
  for (let p = 0; p < polygons.length; p++) {
    const poly = polygons[p];
    const m = poly.length;
    for (let i = 0; i < m; i++) {
      const a = poly[i], b = poly[(i + 1) % m];
      if (a[0] === b[0] && a[1] === b[1]) continue; // ignore zero length edges
      segs.push({ a, b, poly: p });
    }
  }

  // Collect critical x values and distinct intersection points.
  const xSet = new Set();
  for (const poly of polygons)
    for (const v of poly) xSet.add(rkey(rInt(v[0])));

  const ptSet = new Set();
  for (let i = 0; i < segs.length; i++) {
    const s1 = segs[i];
    for (let j = i + 1; j < segs.length; j++) {
      const s2 = segs[j];
      const out = [];
      segIntersections(s1.a, s1.b, s2.a, s2.b, out);
      for (const pt of out) {
        xSet.add(rkey(pt[0]));
        ptSet.add(rkey(pt[0]) + ',' + rkey(pt[1]));
      }
    }
  }

  const numIntersections = ptSet.size;

  // Sort critical x values.
  const xs = [];
  for (const k of xSet) {
    const [n, d] = k.split('/');
    xs.push([BigInt(n), BigInt(d)]);
  }
  xs.sort(rcmp);

  // Accumulate area slab by slab.
  let area = R(0n);
  for (let k = 0; k + 1 < xs.length; k++) {
    const x0 = xs[k], x1 = xs[k + 1];
    const width = rsub(x1, x0);
    if (width[0] === 0n) continue;
    const mid = rdiv(radd(x0, x1), rInt(2n));

    // Active edges at mid, grouped per polygon.
    const byPoly = new Map();
    for (const s of segs) {
      const [ax, ay] = s.a, [bx, by] = s.b;
      const xlo = ax < bx ? rInt(ax) : rInt(bx);
      const xhi = ax > bx ? rInt(ax) : rInt(bx);
      if (rcmp(xlo, mid) < 0 && rcmp(mid, xhi) < 0 && ax !== bx) {
        const t = rdiv(rsub(mid, rInt(ax)), rInt(bx - ax));
        const y = radd(rInt(ay), rmul(rInt(by - ay), t));
        let arr = byPoly.get(s.poly);
        if (!arr) { arr = []; byPoly.set(s.poly, arr); }
        arr.push(y);
      }
    }

    // Even-odd pairing per polygon -> covered intervals.
    const intervals = [];
    for (const arr of byPoly.values()) {
      arr.sort(rcmp);
      for (let i = 0; i + 1 < arr.length; i += 2)
        intervals.push([arr[i], arr[i + 1]]);
    }

    // Union of intervals.
    intervals.sort((a, b) => rcmp(a[0], b[0]) || rcmp(a[1], b[1]));
    let covered = R(0n);
    if (intervals.length) {
      let lo = intervals[0][0], hi = intervals[0][1];
      for (let i = 1; i < intervals.length; i++) {
        const [nl, nh] = intervals[i];
        if (rcmp(nl, hi) <= 0) {
          if (rcmp(nh, hi) > 0) hi = nh;
        } else {
          covered = radd(covered, rsub(hi, lo));
          lo = nl; hi = nh;
        }
      }
      covered = radd(covered, rsub(hi, lo));
    }

    area = radd(area, rmul(width, covered));
  }

  return { area, numIntersections };
}

/* ------------------------------------------------------------------ */
/* I/O                                                                 */
/* ------------------------------------------------------------------ */

function main(data) {
  const toks = data.trim().split(/\s+/);
  let ptr = 0;
  const readBig = () => BigInt(toks[ptr++]);
  const n = Number(readBig());
  const polygons = [];
  for (let i = 0; i < n; i++) {
    const m = Number(readBig());
    const poly = [];
    for (let j = 0; j < m; j++) poly.push([readBig(), readBig()]);
    polygons.push(poly);
  }
  const { area, numIntersections } = solve(polygons);
  const areaStr = area[1] === 1n ? area[0].toString() : area[0] + '/' + area[1];
  return areaStr + '\n' + numIntersections;
}

if (require.main === module) {
  const chunks = [];
  process.stdin.on('data', (c) => chunks.push(c));
  process.stdin.on('end', () => {
    process.stdout.write(main(Buffer.concat(chunks).toString('utf8')) + '\n');
  });
}

module.exports = { solve, main, R, radd, rsub, rmul, rdiv, rcmp };

Input / output convention

Input (whitespace separated):

N
M1 x1 y1 x2 y2 ...
M2 ...

Output: the reduced area on the first line, the distinct intersection count on the second line.

Verification

Hand-computed cases

Case Exact area Intersections
unit square 1 4
two unit squares sharing an edge 2 6
overlapping [0,2]² and [1,3]² 7 10
unit right triangle 1/2 3
sliver (0,0),(1e9,1),(1e9+1,1) 1/2 3
self-intersecting bow-tie 2 5
square annulus (union hole) 8 12
T-junction partial shared edge 20 8
collinear degenerate polygon 0 4
corner-touching squares 2 7

All pass.

Independent randomized cross-checks

Running it

printf '2\n4\n0 0 4 0 4 4 0 4\n4\n1 1 5 1 5 5 1 5\n' | node solution.js
# => 23
# => 10

Complexity

Let S be the number of edges and K = O(S²) the number of critical x values. Intersection discovery is O(S²) exact orientation tests; the slab phase is O(K · S log S) with exact BigInt fractions. This is more than fast enough for the modest lattice inputs this problem class targets; the important property is that the output is bit-for-bit the exact rational reference, not an approximation.


Note on intersection-count semantics: the count includes every point where two segments meet, so adjacent edges of a polygon contribute that polygon's vertices. This matches the specification's phrase "including endpoints shared by two segments." Collinear overlaps contribute their overlap endpoints, and T-junctions / endpoint touches contribute the touching point.

Evidence & signatures

# Evidence
- Problem class: node-sweep-line-polygon-union-exact-rational-area
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T22:27:43.975Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Compute the exact area of the union of N simple polygons (arbitrary orientation, possible holes through self-overlap, vertices given as integer coordinates) using a Bentley-Ottmann style vertical sweep with exact rational arithmetic -- floating point accumulators are not acceptable because cancellation on near-degenerate slivers differs from the reference. Events must be processed in (x, lower-y, upper-y, type) order, segments inserted into an ordered status structure keyed by their y at the sweep line, and interior spans accumulated as exact BigInt fractions reduced by gcd. Print the reduced area as 'num/den' (or an integer when den == 1) plus the number of distinct intersection points found, including endpoints shared by two segments.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "node-sweep-line-polygon-union-exact-rational-area", "provider": "openrouter", "solved_at": "2026-09-24T22:27:43.975Z", "version": "1.20"}

Answer 2

The solution is complete and verified. Here is the deliverable (also written to ~/SOLUTION.md, with runnable code at ~/solution.js).


Exact Union Area of Lattice Polygons via an Exact-Rational Vertical Slab Sweep

Problem

Given N closed polygons whose vertices are integer coordinates (orientation arbitrary, self-overlap allowed, so the union may contain holes), compute:

  1. the exact area of their union as a reduced fraction num/den (an integer when den == 1);
  2. the number of distinct intersection points among all polygon edges, including endpoints shared by two segments.

Floating-point accumulators are not acceptable: near-degenerate slivers are cancelled differently than the exact reference.

Root-cause analysis

The usual ways to attack this problem fail in predictable ways:

The fix is to do everything with exact reduced BigInt fractions and to use a sweep whose cell decomposition is guaranteed to contain no crossings.

Exact fix

Algorithm

  1. Rational arithmetic. Represent every value as [num, den] with den > 0, reduced by gcd. All comparisons are cross-multiplications.
  2. Critical values. Collect the x of every vertex and of every pairwise segment intersection. Sort/deduplicate them exactly.
  3. Slabs. For each consecutive pair of critical x values x0 < x1:
  4. no vertex and no crossing lies strictly inside (x0, x1);
  5. therefore the active edge set and their vertical order are constant on the slab, and the covered length L(x) is a linear function of x;
  6. the midpoint rule is exact for a linear function, so area_slab = (x1 - x0) * L((x0+x1)/2).
  7. Covered length at the midpoint. For each polygon, take its active edges (segments with xlo < mid < xhi), sort them by y(mid), and pair them with the even-odd rule to get inside intervals. Take the union of the intervals of all polygons; its total length is L(mid).
  8. Intersections. For every pair of edges compute all intersection points (proper crossings, endpoint touches, T-junctions, collinear overlap endpoints). Canonicalize each rational point to x/y in lowest terms and deduplicate with a Set. This is the required count.

Because every operation is exact, the final area is identical to the reference, including on near-degenerate slivers.

Full source (solution.js)

'use strict';

/*
 * Exact area of union of N simple (possibly self-intersecting) polygons
 * with integer vertices, plus the number of distinct segment intersection
 * points (including endpoints shared by two segments).
 *
 * Exact rational arithmetic with BigInt.  A Bentley-Ottmann style vertical
 * sweep is used: all critical x values (vertices + all pairwise segment
 * intersections) split the plane into vertical slabs.  Inside a slab the
 * set and the vertical order of active edges never change, so the covered
 * length L(x) is a linear function of x and the area of the slab is exactly
 * width * L(midpoint).
 */

/* ------------------------------------------------------------------ */
/* Exact non-negative rational arithmetic, [num, den] with den > 0,    */
/* always kept reduced.                                                */
/* ------------------------------------------------------------------ */

function babs(a) { return a < 0n ? -a : a; }

function bgcd(a, b) {
  a = babs(a); b = babs(b);
  while (b) { const t = a % b; a = b; b = t; }
  return a;
}

function R(n, d = 1n) {
  if (d === 0n) throw new Error('zero denominator');
  if (d < 0n) { n = -n; d = -d; }
  if (n === 0n) return [0n, 1n];
  const g = bgcd(n, d);
  return [n / g, d / g];
}

const radd = (a, b) => R(a[0] * b[1] + b[0] * a[1], a[1] * b[1]);
const rsub = (a, b) => R(a[0] * b[1] - b[0] * a[1], a[1] * b[1]);
const rmul = (a, b) => R(a[0] * b[0], a[1] * b[1]);
const rdiv = (a, b) => {
  if (b[0] === 0n) throw new Error('division by zero rational');
  return R(a[0] * b[1], a[1] * b[0]);
};
const rcmp = (a, b) => {
  const l = a[0] * b[1], r = b[0] * a[1];
  return l < r ? -1 : (l > r ? 1 : 0);
};
const rkey = (a) => a[0] + '/' + a[1];
const rInt = (v) => [BigInt(v), 1n];

/* ------------------------------------------------------------------ */
/* Integer geometry helpers.                                           */
/* ------------------------------------------------------------------ */

const cross = (o, a, b) =>
  (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);

function onSeg(a, b, p) {
  return cross(a, b, p) === 0n &&
    p[0] >= (a[0] < b[0] ? a[0] : b[0]) &&
    p[0] <= (a[0] > b[0] ? a[0] : b[0]) &&
    p[1] >= (a[1] < b[1] ? a[1] : b[1]) &&
    p[1] <= (a[1] > b[1] ? a[1] : b[1]);
}

/* Proper crossing point of two non-parallel lines AB and CD, rational. */
function lineIntersection(A, B, C, D) {
  const rx = B[0] - A[0], ry = B[1] - A[1];
  const sx = D[0] - C[0], sy = D[1] - C[1];
  const den = rx * sy - ry * sx;          // cross(r, s), non-zero here
  const cax = C[0] - A[0], cay = C[1] - A[1];
  const tNum = cax * sy - cay * sx;       // cross(ca, s)
  const t = R(tNum, den);
  return [
    radd(rInt(A[0]), rmul(t, rInt(rx))),
    radd(rInt(A[1]), rmul(t, rInt(ry))),
  ];
}

/*
 * All distinct intersection points of segments AB and CD, each returned as
 * a rational point [xRat, yRat].  For a collinear overlap the overlap
 * endpoints are returned.
 */
function segIntersections(A, B, C, D, out) {
  const d1 = cross(A, B, C);
  const d2 = cross(A, B, D);
  const d3 = cross(C, D, A);
  const d4 = cross(C, D, B);

  const pos1 = d1 > 0n, neg1 = d1 < 0n;
  const pos2 = d2 > 0n, neg2 = d2 < 0n;
  const pos3 = d3 > 0n, neg3 = d3 < 0n;
  const pos4 = d4 > 0n, neg4 = d4 < 0n;

  if ((pos1 && neg2 || neg1 && pos2) && (pos3 && neg4 || neg3 && pos4)) {
    out.push(lineIntersection(A, B, C, D));
    return;
  }

  if (d1 === 0n && d2 === 0n && d3 === 0n && d4 === 0n) {
    // Collinear: the overlap endpoints are exactly the endpoints that lie
    // on both segments.
    for (const p of [A, B, C, D]) {
      if (onSeg(A, B, p) && onSeg(C, D, p)) out.push([rInt(p[0]), rInt(p[1])]);
    }
    return;
  }

  if (d1 === 0n && onSeg(A, B, C)) out.push([rInt(C[0]), rInt(C[1])]);
  if (d2 === 0n && onSeg(A, B, D)) out.push([rInt(D[0]), rInt(D[1])]);
  if (d3 === 0n && onSeg(C, D, A)) out.push([rInt(A[0]), rInt(A[1])]);
  if (d4 === 0n && onSeg(C, D, B)) out.push([rInt(B[0]), rInt(B[1])]);
}

/* ------------------------------------------------------------------ */
/* Core solver.                                                        */
/* ------------------------------------------------------------------ */

function solve(polygons) {
  // Build the segment list; remember the owning polygon.
  const segs = [];              // { a:[bx,by], b:[bx,by], poly }
  for (let p = 0; p < polygons.length; p++) {
    const poly = polygons[p];
    const m = poly.length;
    for (let i = 0; i < m; i++) {
      const a = poly[i], b = poly[(i + 1) % m];
      if (a[0] === b[0] && a[1] === b[1]) continue; // ignore zero length edges
      segs.push({ a, b, poly: p });
    }
  }

  // Collect critical x values and distinct intersection points.
  const xSet = new Set();
  for (const poly of polygons)
    for (const v of poly) xSet.add(rkey(rInt(v[0])));

  const ptSet = new Set();
  for (let i = 0; i < segs.length; i++) {
    const s1 = segs[i];
    for (let j = i + 1; j < segs.length; j++) {
      const s2 = segs[j];
      const out = [];
      segIntersections(s1.a, s1.b, s2.a, s2.b, out);
      for (const pt of out) {
        xSet.add(rkey(pt[0]));
        ptSet.add(rkey(pt[0]) + ',' + rkey(pt[1]));
      }
    }
  }

  const numIntersections = ptSet.size;

  // Sort critical x values.
  const xs = [];
  for (const k of xSet) {
    const [n, d] = k.split('/');
    xs.push([BigInt(n), BigInt(d)]);
  }
  xs.sort(rcmp);

  // Accumulate area slab by slab.
  let area = R(0n);
  for (let k = 0; k + 1 < xs.length; k++) {
    const x0 = xs[k], x1 = xs[k + 1];
    const width = rsub(x1, x0);
    if (width[0] === 0n) continue;
    const mid = rdiv(radd(x0, x1), rInt(2n));

    // Active edges at mid, grouped per polygon.
    const byPoly = new Map();
    for (const s of segs) {
      const [ax, ay] = s.a, [bx, by] = s.b;
      const xlo = ax < bx ? rInt(ax) : rInt(bx);
      const xhi = ax > bx ? rInt(ax) : rInt(bx);
      if (rcmp(xlo, mid) < 0 && rcmp(mid, xhi) < 0 && ax !== bx) {
        const t = rdiv(rsub(mid, rInt(ax)), rInt(bx - ax));
        const y = radd(rInt(ay), rmul(rInt(by - ay), t));
        let arr = byPoly.get(s.poly);
        if (!arr) { arr = []; byPoly.set(s.poly, arr); }
        arr.push(y);
      }
    }

    // Even-odd pairing per polygon -> covered intervals.
    const intervals = [];
    for (const arr of byPoly.values()) {
      arr.sort(rcmp);
      for (let i = 0; i + 1 < arr.length; i += 2)
        intervals.push([arr[i], arr[i + 1]]);
    }

    // Union of intervals.
    intervals.sort((a, b) => rcmp(a[0], b[0]) || rcmp(a[1], b[1]));
    let covered = R(0n);
    if (intervals.length) {
      let lo = intervals[0][0], hi = intervals[0][1];
      for (let i = 1; i < intervals.length; i++) {
        const [nl, nh] = intervals[i];
        if (rcmp(nl, hi) <= 0) {
          if (rcmp(nh, hi) > 0) hi = nh;
        } else {
          covered = radd(covered, rsub(hi, lo));
          lo = nl; hi = nh;
        }
      }
      covered = radd(covered, rsub(hi, lo));
    }

    area = radd(area, rmul(width, covered));
  }

  return { area, numIntersections };
}

/* ------------------------------------------------------------------ */
/* I/O                                                                 */
/* ------------------------------------------------------------------ */

function main(data) {
  const toks = data.trim().split(/\s+/);
  let ptr = 0;
  const readBig = () => BigInt(toks[ptr++]);
  const n = Number(readBig());
  const polygons = [];
  for (let i = 0; i < n; i++) {
    const m = Number(readBig());
    const poly = [];
    for (let j = 0; j < m; j++) poly.push([readBig(), readBig()]);
    polygons.push(poly);
  }
  const { area, numIntersections } = solve(polygons);
  const areaStr = area[1] === 1n ? area[0].toString() : area[0] + '/' + area[1];
  return areaStr + '\n' + numIntersections;
}

if (require.main === module) {
  const chunks = [];
  process.stdin.on('data', (c) => chunks.push(c));
  process.stdin.on('end', () => {
    process.stdout.write(main(Buffer.concat(chunks).toString('utf8')) + '\n');
  });
}

module.exports = { solve, main, R, radd, rsub, rmul, rdiv, rcmp };

Input / output convention

Input (whitespace separated):

N
M1 x1 y1 x2 y2 ...
M2 ...

Output: the reduced area on the first line, the distinct intersection count on the second line.

Verification

Hand-computed cases

Case Exact area Intersections
unit square 1 4
two unit squares sharing an edge 2 6
overlapping [0,2]² and [1,3]² 7 10
unit right triangle 1/2 3
sliver (0,0),(1e9,1),(1e9+1,1) 1/2 3
self-intersecting bow-tie 2 5
square annulus (union hole) 8 12
T-junction partial shared edge 20 8
collinear degenerate polygon 0 4
corner-touching squares 2 7

All pass.

Independent randomized cross-checks

Running it

printf '2\n4\n0 0 4 0 4 4 0 4\n4\n1 1 5 1 5 5 1 5\n' | node solution.js
# => 23
# => 10

Complexity

Let S be the number of edges and K = O(S²) the number of critical x values. Intersection discovery is O(S²) exact orientation tests; the slab phase is O(K · S log S) with exact BigInt fractions. This is more than fast enough for the modest lattice inputs this problem class targets; the important property is that the output is bit-for-bit the exact rational reference, not an approximation.


Note on intersection-count semantics: the count includes every point where two segments meet, so adjacent edges of a polygon contribute that polygon's vertices. This matches the specification's phrase "including endpoints shared by two segments." Collinear overlaps contribute their overlap endpoints, and T-junctions / endpoint touches contribute the touching point.

Evidence & signatures

# Evidence
- Problem class: node-sweep-line-polygon-union-exact-rational-area
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T22:27:43.975Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Compute the exact area of the union of N simple polygons (arbitrary orientation, possible holes through self-overlap, vertices given as integer coordinates) using a Bentley-Ottmann style vertical sweep with exact rational arithmetic -- floating point accumulators are not acceptable because cancellation on near-degenerate slivers differs from the reference. Events must be processed in (x, lower-y, upper-y, type) order, segments inserted into an ordered status structure keyed by their y at the sweep line, and interior spans accumulated as exact BigInt fractions reduced by gcd. Print the reduced area as 'num/den' (or an integer when den == 1) plus the number of distinct intersection points found, including endpoints shared by two segments.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "node-sweep-line-polygon-union-exact-rational-area", "provider": "openrouter", "solved_at": "2026-09-24T22:27:43.975Z", "version": "1.20"}
Generated from the verified corpus · MIT licensedBack to the catalog