From ef76446306d7bda390a195dd7447b2561d35f50c Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Fri, 12 Dec 2025 00:46:55 +0100 Subject: [PATCH] Part 2 working --- src/day09.scala | 183 ++++++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 82 deletions(-) diff --git a/src/day09.scala b/src/day09.scala index 625179d..7ace445 100644 --- a/src/day09.scala +++ b/src/day09.scala @@ -12,98 +12,117 @@ val dayNumber = "09" println(part2(loadInput(dayNumber))) def part1(input: String): Long = - val tiles = input.split('\n').map { case s"$x,$y" => (x = x.toLong, y = y.toLong) } - tiles + input + .split('\n').map { case s"$x,$y" => (x = x.toLong, y = y.toLong) } .combinations(2) - .collect { - case Array(a, b) if a != b => - ((a.x - b.x + 1) * (a.y - b.y + 1)).abs - }.max - -enum Edge: - case H(fixedY: Int, range: Range) - case V(fixedX: Int, range: Range) - - def contains(x: Int, y: Int): Boolean = - this match - case H(fixedY, range) => fixedY == y && range.contains(x) - case V(fixedX, range) => fixedX == x && range.contains(y) - -object Edge: - def apply(a: Point, b: Point): Edge = - if a.y == b.y then Edge.H(a.y, min(a.x, b.x) to max(a.x, b.x)) - else if a.x == b.x then Edge.V(a.x, min(a.y, b.y) to max(a.y, b.y)) - else ??? - -case class Polygon(edges: List[Edge]): - val minX = edges.map { - case Edge.H(_, range) => range.min - case Edge.V(fixedX, _) => fixedX - }.min - 1 - - def contains(p: Point) = - if edges.exists(_.contains(p.x, p.y)) then true - else - // println(p) - val (count, _) = (minX to p.x).foldLeft((0, false)) { case ((count, switched), x) => - // println(x) - // print("edge contains ") - // println(edges.exists(_.contains(x, p.y))) - // print("switched ") - // println(switched) - if edges.exists(_.contains(x, p.y)) && !switched then (count + 1, true) - else (count, false) - } - // println("count") - // println(count) - count % 2 == 1 - -case class Point(x: Int, y: Int) + .collect { case Array(a, b) => ((a.x - b.x + 1) * (a.y - b.y + 1)).abs } + .max def part2(input: String): Long = - val points = input.split('\n').map { case s"$x,$y" => Point(x.toInt, y.toInt) } - val polygon = Polygon( - (points :+ points.head).sliding(2).map { case Array(a, b) => Edge(a, b) }.toList - ) - def corners(a: Point, b: Point) = - List( - Point(min(a.x, b.x), min(a.y, b.y)), - Point(min(a.x, b.x), max(a.y, b.y)), - Point(max(a.x, b.x), min(a.y, b.y)), - Point(max(a.x, b.x), max(a.y, b.y)) - ).distinct + case class Point(x: Int, y: Int) - // println(polygon.contains(Point(9, 3))) + enum Edge: + case H(y: Int, xStart: Int, xEnd: Int) + case V(x: Int, yStart: Int, yEnd: Int) - // println(polygon.edges.mkString("\n")) + object Edge: + def apply(a: Point, b: Point): Edge = + if a.y == b.y then + val xMin = min(a.x, b.x) + val xMax = max(a.x, b.x) + Edge.H(a.y, xMin, xMax) + else if a.x == b.x then + val yMin = min(a.y, b.y) + val yMax = max(a.y, b.y) + Edge.V(a.x, yMin, yMax) + else throw new IllegalArgumentException("Edges must be axis-aligned") - // println(polygon.edges.exists(_.contains(9, 3))) - // println(polygon.edges.exists(_.contains(9, 5))) - // println(polygon.contains(Point(11, 1))) - // println(polygon.contains(Point(11, 7))) - // println(polygon.contains(Point(11, 8))) - // println(polygon.contains(Point(11, 9))) + // Build the polygon edges out of the ordered red points. + val points = input.split('\n').map { case s"$x,$y" => Point(x.toInt, y.toInt) } + val edges = (points :+ points.head).sliding(2).map { case Array(a, b) => Edge(a, b) }.toList + val verticalEdges = edges.collect { case v: Edge.V => v } - // corners(Point(9, 5), Point(2, 3)).foreach(p => - // println(p) - // println(polygon.contains(p)) - // ) + // Coordinate compression: keep only x or y values where something changes + // (a boundary or its immediately-adjacent value). Each consecutive pair in `xs`/`ys` + // represents one strip of actual tiles in the original grid. + val xs = (points.map(_.x) ++ points.map(_.x + 1)).distinct.sorted + val ys = (points.map(_.y) ++ points.map(_.y + 1)).distinct.sorted - println( - points - .combinations(2) - .map { case Array(a, b) => - corners(a, b) - }.distinct.size - ) + val xIndex = xs.zipWithIndex.toMap + val yIndex = ys.zipWithIndex.toMap + + // allowed(i)(j) tells us whether the compressed cell spanning + // x in [xs(i), xs(i+1)) and y in [ys(j), ys(j+1)) belongs to the red/green region. + val allowed = Array.ofDim[Boolean](xs.length - 1, ys.length - 1) + + // Even–odd scanline: for each horizontal strip, walk the vertical edges to mark the + // interior spans between every pair of crossings. + for j <- 0 until ys.length - 1 do + val ySample = ys(j) + val crossings = verticalEdges.collect { + case Edge.V(x, yStart, yEnd) if ySample >= yStart && ySample < yEnd => x + }.sorted + crossings.grouped(2).foreach { + case List(left, right) => + val start = xIndex(left) + 1 + val end = xIndex(right) + if start < end then for i <- start until end do allowed(i)(j) = true + case _ => + } + + // Include the boundary itself so rectangles that ride along the green loop remain valid. + edges.foreach { + case Edge.H(y, xStart, xEnd) => + val xiStart = xIndex(xStart) + val xiEnd = xIndex(xEnd + 1) + val yjStart = yIndex(y) + val yjEnd = yIndex(y + 1) + for i <- xiStart until xiEnd do for j <- yjStart until yjEnd do allowed(i)(j) = true + case Edge.V(x, yStart, yEnd) => + val xiStart = xIndex(x) + val xiEnd = xIndex(x + 1) + val yjStart = yIndex(yStart) + val yjEnd = yIndex(yEnd + 1) + for i <- xiStart until xiEnd do for j <- yjStart until yjEnd do allowed(i)(j) = true + } + + // Convert the boolean grid into exact areas (width × height of each compressed cell). + val cellArea = Array.ofDim[Long](xs.length - 1, ys.length - 1) + for + i <- 0 until xs.length - 1 + j <- 0 until ys.length - 1 + do + if allowed(i)(j) then + val width = (xs(i + 1) - xs(i)).toLong + val height = (ys(j + 1) - ys(j)).toLong + cellArea(i)(j) = width * height + + // 2D prefix sums let us query the total allowed area of any rectangle in O(1). + val prefix = Array.ofDim[Long](xs.length, ys.length) + for + i <- 0 until xs.length - 1 + j <- 0 until ys.length - 1 + do + val value = cellArea(i)(j) + prefix(i + 1)(j + 1) = value + prefix(i)(j + 1) + prefix(i + 1)(j) - prefix(i)(j) + + def areaWithin(xStart: Int, yStart: Int, xEnd: Int, yEnd: Int): Long = + prefix(xEnd)(yEnd) - prefix(xStart)(yEnd) - prefix(xEnd)(yStart) + prefix(xStart)(yStart) + + // Try every pair of red tiles and keep the rectangle only if every compressed cell inside + // is marked allowed (i.e. area reported by prefix sum equals the rectangle's true area). points .combinations(2) - .collect { - case Array(a, b) if a != b && corners(a, b).forall(polygon.contains) => - println(corners(a, b)) - (a, b) + .foldLeft(0L) { + case (best, Array(a, b)) => + val xMin = min(a.x, b.x) + val xMax = max(a.x, b.x) + val yMin = min(a.y, b.y) + val yMax = max(a.y, b.y) + val area = (xMax - xMin + 1).toLong * (yMax - yMin + 1).toLong + val insideArea = areaWithin(xIndex(xMin), yIndex(yMin), xIndex(xMax + 1), yIndex(yMax + 1)) + if area == insideArea then math.max(best, area) else best + case (best, _) => best } - .map((a, b) => ((a.x - b.x + 1) * (a.y - b.y + 1)).abs) - .max end part2