this post was submitted on 09 Dec 2025
14 points (85.0% liked)

Advent Of Code

1186 readers
29 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

Day 9: Movie Theater

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

top 15 comments
sorted by: hot top controversial new old
[–] addie@feddit.uk 5 points 1 day ago* (last edited 1 day ago)

C++

Correct answer in under 2 seconds, ie. about a hundred times longer than the rest of the puzzles so far combined. Can't think of a more efficient way to do this; no doubt it'll come to me at two in the morning, like usual.

Part 2 implementation breaks down the 'green tiles outline' into a series of line segments and then uses the even-odd winding rule to decide whether a point is inside or outside. Tracing the outline of the potential boxes to see whether the entirety is inside allows selection of the largest. We can skip tracing any potential box that wouldn't be bigger than what we have so far, and since the tests are easy to parallelise, have done so.

#include <atomic>
#include <boost/log/trivial.hpp>
#include <boost/unordered/concurrent_flat_map.hpp>
#include <cstddef>
#include <fstream>
#include <mutex>
#include <thread>

namespace {

struct Point {
  int x, y;
  auto operator+=(const Point &b) -> Point & {
    x += b.x;
    y += b.y;
    return *this;
  }
};

auto operator==(const Point &a, const Point &b) {
  return a.x == b.x && a.y == b.y;
}

auto operator!=(const Point &a, const Point &b) { return !(a == b); }

std::size_t hash_value(const Point &p) {
  return size_t(p.x) << 32 | size_t(p.y);
}

using Map = std::vector<Point>;
struct Line {
  Point a, b;
};

auto read() {
  auto rval = std::vector<Point>{};
  auto ih = std::ifstream{"09.txt"};
  auto line = std::string{};
  while (std::getline(ih, line)) {
    auto c1 = line.find(',');
    rval.emplace_back(
        std::stoi(line.substr(0, c1)), std::stoi(line.substr(c1 + 1))
    );
  }
  return rval;
}

auto size(const Point &a, const Point &b) -> uint64_t {
  size_t w = std::abs(a.x - b.x) + 1;
  size_t h = std::abs(a.y - b.y) + 1;
  return w * h;
}

auto part1(const std::vector<Point> &p) {
  auto maxi = std::size_t{};
  for (auto a = size_t{}; a < p.size() - 1; ++a)
    for (auto b = a + 1; b < p.size(); ++b)
      maxi = std::max(maxi, size(p[a], p[b]));
  return maxi;
}

auto make_line(const Point &a, const Point &b) {
  return Line{
      {std::min(a.x, b.x), std::min(a.y, b.y)},
      {std::max(a.x, b.x), std::max(a.y, b.y)}
  };
}

auto direction(const Point &a, const Point &b) {
  auto dx = b.x - a.x;
  auto dy = b.y - a.y;
  if (dx != 0)
    dx /= std::abs(dx);
  if (dy != 0)
    dy /= std::abs(dy);
  return Point{dx, dy};
}

// evaluates 'insideness' with the even-odd rule.  On the line -> inside
auto inside_uncached(const Point &a, const std::vector<Line> &lines) -> bool {
  auto even_odd = 0;
  for (const auto &line : lines) {
    if (line.a.y == line.b.y) { // horizontal
      if (a.y != line.a.y)
        continue;
      if (a.x <= line.a.x)
        continue;
      if (a.x < line.b.x)
        return true;
      even_odd += 1;
    } else { // vertical
      if (a.x < line.a.x)
        continue;
      if (a.y < line.a.y || a.y > line.b.y)
        continue;
      if (a.x == line.a.x)
        return true;
      even_odd += 1;
    }
  }
  return even_odd % 2 == 1;
}

using Cache = boost::unordered::concurrent_flat_map<Point, bool>;
auto cache = Cache{};

auto inside(const Point &a, const std::vector<Line> &lines) -> bool {
  std::optional<bool> o;
  bool found = cache.visit(a, [&](const auto &x) { o = x.second; });
  if (found)
    return o.value();
  auto b = inside_uncached(a, lines);
  cache.insert({a, b});
  return b;
}

auto inside(const Line &a, const std::vector<Line> &lines) -> bool {
  auto dir = direction(a.a, a.b);
  auto point = a.a;
  while (point != a.b) {
    point += dir;
    if (!inside(point, lines))
      return false;
  }
  return true;
}

auto part2(std::vector<Point> p) {
  auto outline = std::vector<Line>{};

  for (auto a = size_t{}; a < p.size() - 1; ++a)
    outline.push_back(make_line(p[a], p[a + 1]));
  outline.push_back(make_line(p.back(), p.front()));

  std::sort(outline.begin(), outline.end(), [](auto &a, auto &b) {
    return a.a.x < b.a.x;
  });

  std::sort(p.begin(), p.end(), [](auto &a, auto &b) {
    if (a.x != b.x)
      return a.x < b.x;
    return a.y > b.y;
  });

  auto maximum = std::atomic_uint64_t{};
  auto update_lock = std::mutex{};
  auto column_worker = std::atomic_uint64_t{};
  auto threadpool = std::vector<std::thread>{};

  for (auto t = size_t{}; t < std::thread::hardware_concurrency(); ++t) {
    threadpool.push_back(std::thread([&]() {
      while (true) {
        auto a = column_worker++;
        if (a > p.size() - 1)
          break;
        for (auto b = p.size() - 1; b > a; --b) {
          auto box = size(p[a], p[b]);

          // if it wouldn't be bigger, skip it
          if (box <= maximum)
            continue;

          // quick check for the opposite corners
          if (!inside(Point{p[a].x, p[b].y}, outline) ||
              !inside(Point{p[b].x, p[a].y}, outline))
            continue;

          // trace the outline
          auto left = make_line(p[a], Point{p[a].x, p[b].y});
          if (!inside(left, outline))
            continue;
          auto top = make_line(Point{p[a].x, p[b].y}, p[b]);
          if (!inside(top, outline))
            continue;
          auto right = make_line(Point{p[b].x, p[a].y}, p[b]);
          if (!inside(right, outline))
            continue;
          auto bottom = make_line(p[a], Point{p[b].x, p[a].y});
          if (!inside(bottom, outline))
            continue;

          // it's all on green tiles.  update as the biggest so far
          {
            auto _ = std::lock_guard<std::mutex>(update_lock);
            if (box > maximum)
              maximum = box;
          }
        }
      }
    }));
  }

  for (auto &thread : threadpool)
    thread.join();

  return uint64_t(maximum);
}

} // namespace

auto main() -> int {
  auto tiles = read();
  BOOST_LOG_TRIVIAL(info) << "Day 9: read " << tiles.size();
  BOOST_LOG_TRIVIAL(info) << "1: " << part1(tiles);
  BOOST_LOG_TRIVIAL(info) << "2: " << part2(tiles);
}
[–] skissue@programming.dev 2 points 23 hours ago

Uiua

My silly part one solution, because I have no idea how to do part two (this is usually the difficulty at which I bow out, but I want to try actually expanding my knowledge this year!).

Parse         ← ⊜(βŠœβ‹•βŠΈβ‰ @,)βŠΈβ‰ @\n
CornerClosest ← βŠβŠ’ββŠΈβ‰‘βŒž(/+ⁿ2-)
Big           ← 9999999
RectSize      ← /Γ—+1⌡-
Part₁ ← (
  ∩⌟∩⌟CornerClosest 0_0 1000000_1000000 Big_0 0_Big
  β†₯∩RectSize
)

$ 7,1
$ 11,1
$ 11,7
$ 9,7
$ 9,5
$ 2,5
$ 2,3
$ 7,3

&fras "9.txt"
Part₁ Parse
[–] Gobbel2000@programming.dev 2 points 23 hours ago

Rust

This one broke me, I couldn't do it without looking at some of the solutions in this thread. In the end, basically all that was necessary for part 2 is to check for every candidate rectangle if:

  1. No corners (red tiles) are inside the rectangle, and
  2. No lines intersect the rectangle.

Plus a bunch shifting numbers around by 1. The code is not pretty at all, but at least it turned very efficient, solving part 2 in just 4.3ms. I have no idea how generalized the solution is. It definitely assumes that any larger rectangle is spanned inside the area and that the path doesn't cross over itself. Also of course that there are no two corners next to each other forming a 180 degree turn.

View on github

Code

use std::ops::{Range, RangeInclusive};

fn parse_input(input: &str) -> Vec<(u32, u32)> {
    input
        .lines()
        .map(|l| {
            let (a, b) = l.split_once(',').unwrap();
            (a.parse::<u32>().unwrap(), b.parse::<u32>().unwrap())
        })
        .collect()
}

#[inline]
fn area(a: (u32, u32), b: (u32, u32)) -> u64 {
    (a.0.abs_diff(b.0) as u64 + 1) * (a.1.abs_diff(b.1) as u64 + 1)
}

fn part1(input: String) {
    let tiles = parse_input(&input);
    let mut largest = 0;
    for t1 in &tiles {
        for t2 in &tiles {
            let a = area(*t1, *t2);
            if a > largest {
                largest = a;
            }
        }
    }
    println!("{largest}");
}

// Returns true only if t is not inside of the rectangle
#[inline]
fn red_allowed(t: (u32, u32), x_range: Range<u32>, y_range: Range<u32>) -> bool {
    !(t.0 > x_range.start && t.0 + 1 < x_range.end && t.1 > y_range.start && t.1 + 1 < y_range.end)
}

fn is_contained(
    a: (u32, u32),
    b: (u32, u32),
    tiles_x: &[(u32, u32)],
    tiles_y: &[(u32, u32)],
    vert_lines: &[(u32, RangeInclusive<u32>)],
    hori_lines: &[(u32, RangeInclusive<u32>)],
) -> bool {
    let x_range = a.0.min(b.0)..(a.0.max(b.0) + 1);
    let y_range = a.1.min(b.1)..(a.1.max(b.1) + 1);

    // Check that no corners (red tiles) are inside the rectangle
    let corners_ok = if x_range.end - x_range.start <= y_range.end - y_range.start {
        // Use tiles_x
        let start = match tiles_x.binary_search(&(x_range.start, 0)) {
            Ok(e) => e,
            Err(e) => e,
        };
        tiles_x
            .iter()
            .skip(start)
            .take_while(|t| t.0 < x_range.end)
            .filter(|&&t| t != a && t != b)
            .all(|t| red_allowed(*t, x_range.clone(), y_range.clone()))
    } else {
        // Use tiles_y
        let start = match tiles_y.binary_search_by_key(&(y_range.start, 0), |(x, y)| (*y, *x)) {
            Ok(e) => e,
            Err(e) => e,
        };
        tiles_y
            .iter()
            .skip(start)
            .take_while(|t| t.1 < y_range.end)
            .filter(|&&t| t != a && t != b)
            .all(|t| red_allowed(*t, x_range.clone(), y_range.clone()))
    };
    if !corners_ok {
        return false;
    }

    // Check that no line intersects the inside of the rectangle
    let start = match vert_lines.binary_search_by_key(&x_range.start, |(x, _)| *x) {
        Ok(e) => e,
        Err(e) => e,
    };
    for (x, line) in vert_lines
        .iter()
        .skip(start)
        .take_while(|(x, _)| *x < x_range.end)
    {
        if x_range.start < *x
            && x_range.end > *x + 1
            && (y_range.start + 1).max(*line.start()) < (y_range.end - 1).min(line.end() + 1)
        {
            return false;
        }
    }
    let start = match hori_lines.binary_search_by_key(&y_range.start, |(y, _)| *y) {
        Ok(e) => e,
        Err(e) => e,
    };
    for (y, line) in hori_lines
        .iter()
        .skip(start)
        .take_while(|(y, _)| *y < y_range.end)
    {
        if y_range.start < *y
            && y_range.end > *y + 1
            && (x_range.start + 1).max(*line.start()) < (x_range.end - 1).min(line.end() + 1)
        {
            return false;
        }
    }
    true
}

fn part2(input: String) {
    let tiles = parse_input(&input);

    let mut vert_lines = Vec::new();
    let mut hori_lines = Vec::new();
    let mut prev = *tiles.last().unwrap();
    for &t in &tiles {
        if t.0 == prev.0 {
            vert_lines.push((t.0, t.1.min(prev.1)..=t.1.max(prev.1)));
        } else {
            debug_assert_eq!(t.1, prev.1);
            hori_lines.push((t.1, t.0.min(prev.0)..=t.0.max(prev.0)));
        }
        prev = t;
    }
    vert_lines.sort_by_key(|(x, _)| *x);
    hori_lines.sort_by_key(|(y, _)| *y);

    let mut tiles_x = tiles.clone();
    tiles_x.sort();
    let mut tiles_y = tiles.clone();
    tiles_y.sort_by_key(|(x, y)| (*y, *x));
    let mut largest = 0;
    for (idx, t1) in tiles.iter().enumerate() {
        for t2 in tiles.iter().take(idx) {
            let a = area(*t1, *t2);
            if a > largest && is_contained(*t1, *t2, &tiles_x, &tiles_y, &vert_lines, &hori_lines) {
                largest = a;
            }
        }
    }
    println!("{largest}");
}

util::aoc_main!();

[–] EnEnCode@programming.dev 3 points 1 day ago* (last edited 1 day ago)

Rust

The problem today just flustered me. Sometimes I'm not convinced I've gotten any better at programming since 2022 (yes, I still used that exact hack of a combine_ranges for this problem) Runs on my Thinkpad P1 Gen2 in 180.9 +- 0.9 ms on external power (or 726.8 +- 2.1 ms on battery; can't remember how I configured performance/battery saver honestly lol)

solution

pub fn day09(input: &str) -> (u64, u64) {
    let mut part1 = 0;
    let mut part2 = 0;
    let mut squares = Vec::new();
    for line in input.trim().lines() {
        let (x, y) = line.split_once(',').unwrap();
        squares.push([x.parse::<u64>().unwrap(), y.parse::<u64>().unwrap()]);
    }

    let mut chunks = squares.chunks_exact(2).peekable();
    let mut lines = Vec::new();
    let peek = chunks.peek().unwrap();
    // dir is true if scanning vertically, false if horizontally
    let dir = peek[0][1] == peek[1][1];
    if !dir {
        debug_assert_eq!(peek[0][0], peek[1][0]);
    }

    // Each line is a tuple where .0 is the index in the scan direction and .1 is the pair of end
    // points for a line perpendicular to the scan direction
    for line in chunks {
        lines.push((
            line[0][dir as usize],
            [line[0][!dir as usize], line[1][!dir as usize]],
        ));
    }
    lines.sort();

    // rot is true if field 0 being less than field 1 enters the shape, false for exiting
    let rot = lines[0].1[0] < lines[0].1[1];

    for idx_1 in 0..squares.len() - 1 {
        'o: for idx_2 in (idx_1 + 1)..squares.len() {
            let s1 = squares[idx_1];
            let s2 = squares[idx_2];
            let area = (s1[0].abs_diff(s2[0]) + 1) * (s1[1].abs_diff(s2[1]) + 1);
            part1 = std::cmp::max(part1, area);

            if area <= part2 {
                continue;
            }
            // Think of "up" as the direction the scan line travels
            let floor = std::cmp::max(s1[dir as usize], s2[dir as usize]);
            let ceil = std::cmp::min(s1[dir as usize], s2[dir as usize]);
            let left_wall = std::cmp::min(s1[!dir as usize], s2[!dir as usize]);
            let right_wall = std::cmp::max(s1[!dir as usize], s2[!dir as usize]);
            let floor_idx = lines.binary_search_by_key(&floor, |(l, _)| *l).unwrap();
            let ceil_idx = lines.binary_search_by_key(&ceil, |(l, _)| *l).unwrap();
  
            // If a line contracts the valid range, check if that contraction intersects the walls,
            // since that necessitates uncolored tiles in the bounding box
            let skip = |line: [u64; 2]| {
                if rot != (line[0] < line[1]) {
                    return (left_wall..right_wall).contains(&line[0])
                        || (left_wall..right_wall).contains(&line[1]);
                }
                false
            };

            for line in &lines[ceil_idx..floor_idx] {
                if skip(line.1) {
                    continue 'o;
                }
            }
            let mut combo_ranges = Vec::new();
            // This `rev` is needed for correctness, although the full input does not seem to care
            // It actually hurts performance a lot :(
            for line in lines[0..=ceil_idx].iter().rev() {
                if skip(line.1) {
                    continue 'o;
                }
                if rot {
                    combo_ranges.push(line.1);
                } else {
                    combo_ranges.push([line.1[1], line.1[0]]);
                }
                combo_ranges = combine_ranges(combo_ranges);
                // If overlapping the target range results in no change of range, then the target
                // range is fully covered and this rectangle is valid
                let mut test_range = combo_ranges.clone();
                test_range.push([left_wall, right_wall]);
                if combo_ranges == combine_ranges(test_range) {
                    part2 = area;
                    continue 'o;
                }
            }
        }
    }
    (part1, part2)
}

[–] chunkystyles@sopuli.xyz 3 points 1 day ago

Kotlin

My solution can't be very good because it took over a minute to run part 2. I used a ray casting algorithm to determine if a point was in the main polygon. I did that for the 2 corners of each rectangle that weren't given as input. If both of those corners were in the polygon, then I checked each point of each edge of that rectangle.

My first stab at this didn't consider the edge points of the rectangle, and after looking at some visualizations of the polygon, I could see where I was going wrong. I wouldn't have considered a rectangle with all 4 corners in the polygon could have an edge that goes outside the polygon without looking at a visualization.

Part 2 code:

fun main() {
    val input = getInput(9)
    val polygon = parseInput1(input)
    val rectangles: MutableList<Rectangle> = mutableListOf()
    for (i in polygon.indices) {
        for (j in i + 1..<polygon.size) {
            rectangles.add(Rectangle(polygon[i], polygon[j]))
        }
    }
    rectangles.sortByDescending { it.area }
    var answer: Rectangle? = null
    for (rectangle in rectangles) {
        if (isInsidePolygon(rectangle.corner3, polygon)
            && isInsidePolygon(rectangle.corner4, polygon)
        ) {
            val edgePoints: MutableList<Pair<Long, Long>> = mutableListOf()
            edgePoints.addAll(getAllPointsBetween(rectangle.corner1, rectangle.corner3))
            edgePoints.addAll(getAllPointsBetween(rectangle.corner1, rectangle.corner4))
            edgePoints.addAll(getAllPointsBetween(rectangle.corner2, rectangle.corner3))
            edgePoints.addAll(getAllPointsBetween(rectangle.corner2, rectangle.corner4))
            var isInside = true
            for (edgePoint in edgePoints) {
                if (!isInsidePolygon(edgePoint, polygon)) {
                    isInside = false
                    break
                }
            }
            if (isInside) {
                answer = rectangle
                break
            }
        }
    }
    println(answer?.area ?: "Whoops")
}

fun parseInput1(input: String): List<Pair<Long, Long>> = input.lines()
    .filter { it.isNotBlank() }
    .map {
        val split = it.split(",")
        split[0].toLong() to split[1].toLong()
    }

data class Rectangle(val corner1: Pair<Long, Long>, val corner2: Pair<Long, Long>) {
    val corner3: Pair<Long, Long> = corner1.first to corner2.second
    val corner4: Pair<Long, Long> = corner2.first to corner1.second
    val area: Long = (1 + abs(corner1.first - corner2.first)) * (1 + abs(corner1.second - corner2.second))
}

fun isInsidePolygon(point: Pair<Long, Long>, polygon: List<Pair<Long, Long>>): Boolean {
    val x = point.first
    val y = point.second
    var intersectionCount = 0L
    for (i in polygon.indices) {
        val p1 = polygon[i]
        val p2 = polygon[(i + 1) % polygon.size]
        if (point == p1 || point == p2 || isOnEdge(point, p1, p2)) {
            return true
        }
        val y1 = p1.second
        val y2 = p2.second
        val crossesRay = (y in y1..<y2) || (y in y2..<y1)
        if (crossesRay) {
            val x1 = p1.first
            val x2 = p2.first
            val xIntersect = (x2 - x1) * (y - y1).toDouble() / (y2 - y1) + x1
            if (x < xIntersect) {
                intersectionCount++
            }
        }
    }
    return intersectionCount % 2L != 0L
}

fun isOnEdge(p: Pair<Long, Long>, p1: Pair<Long, Long>, p2: Pair<Long, Long>): Boolean {
    val crossProduct = (p.second - p1.second) * (p2.first - p1.first) -
            (p.first - p1.first) * (p2.second - p1.second)
    if (crossProduct != 0L) {
        return false
    }
    val isBetweenX = p.first in minOf(p1.first, p2.first)..maxOf(p1.first, p2.first)
    val isBetweenY = p.second in minOf(p1.second, p2.second)..maxOf(p1.second, p2.second)
    return isBetweenX && isBetweenY
}

fun getAllPointsBetween(left: Pair<Long, Long>, right: Pair<Long, Long>): List<Pair<Long, Long>> {
    if (right.first == left.first) {
        val max = maxOf(left.second, right.second)
        val min = minOf(left.second, right.second)
        return (min + 1..<max).toList().map { right.first to it }
    } else if (right.second == left.second) {
        val max = maxOf(left.first, right.first)
        val min = minOf(left.first, right.first)
        return (min + 1..<max).toList().map { it to right.second }
    } else {
        throw Exception("Whoops")
    }
}
[–] Pyro@programming.dev 2 points 1 day ago* (last edited 1 day ago)

Python

Part 2 is only partially optimized (runs in ~7.5s).

The optimization I did do is to compress the grid coords by recording all red tiles and their adjacent cells only (thanks Everybody,Codes #15)
The part which needs further optimization is my approach to getting the largest rectangle in the polygon. I did it by flood-filling the area outside the polygon, then walking through all cells of every pair of red tiles to make sure their rectangle is within the polygon.

click to view code

# combinations helps us get all pairs of red tiles
# pairwise helps us get all adjacent pairs of red tiles
from itertools import combinations, pairwise
from collections import deque

def get_area(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return (abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)

@timer()
def part1(data: str):
    red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]

    max_area = 0
    for r1, r2 in combinations(red_tiles, 2):
        max_area = max(max_area, get_area(r1, r2))
    return max_area

def compress_coordinates(coords: list[tuple[int, int]]):
    i_of_interest_set = set()
    j_of_interest_set = set()

    for i, j in coords:
        i_of_interest_set.update([i-1, i, i+1])
        j_of_interest_set.update([j-1, j, j+1])
    
    sorted_is = sorted(list(i_of_interest_set))
    sorted_js = sorted(list(j_of_interest_set))
    m = len(sorted_is)
    n = len(sorted_js)

    compress_i_map = {val: id for id, val in enumerate(sorted_is)}
    compress_j_map = {val: id for id, val in enumerate(sorted_js)}
    return m, n, compress_i_map, compress_j_map


@timer()
def part2(data: str):
    red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]
    m, n, compress_i_map, compress_j_map = compress_coordinates(red_tiles)

    def get_compressed_coords(og_coords: tuple[int, int]):
        return compress_i_map[og_coords[0]], compress_j_map[og_coords[1]]

    # make the grid
    grid = [['.']*n for _ in range(m)]
    for i, j in red_tiles:
        ci, cj = get_compressed_coords((i, j))
        grid[ci][cj] = '#'

    # make the walls of the polygon enclosed by the red tiles
    def make_line(r1, r2):
        r1_i, r1_j = get_compressed_coords(r1)
        r2_i, r2_j = get_compressed_coords(r2)

        wall_char = '#'
        if r1_i == r2_i:
            # same row
            row = r1_i
            for col in range(min(r1_j, r2_j) + 1, max(r1_j, r2_j)):
                grid[row][col] = wall_char
        elif r1_j == r2_j:
            # same col
            col = r1_j
            for row in range(min(r1_i, r2_i) + 1, max(r1_i, r2_i)):
                grid[row][col] = wall_char

    for r1, r2 in pairwise(red_tiles):
        make_line(r1, r2)
    make_line(red_tiles[-1], red_tiles[0])

    # whiteout the exterior
    DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    queue = deque([(0, 0)])
    while queue:
        i, j = queue.popleft()
        if grid[i][j] == ' ':
            continue
        grid[i][j] = ' '

        for di, dj in DIRECTIONS:
            ni, nj = i+di, j+dj
            if not (0 <= ni < m) or not (0 <= nj < n):
                continue
            if grid[ni][nj] != '.':
                continue
            queue.append((ni, nj))
    
    # DEBUG: print the grid
    # print()
    # for row in grid:
    #     print(''.join(row))
    
    # check if the rectangle formed by the corners r1, r2 is contained within the red-green polygon
    def check_contained(r1, r2):
        r1_i, r1_j = get_compressed_coords(r1)
        r2_i, r2_j = get_compressed_coords(r2)
        
        min_i, max_i = min(r1_i, r2_i), max(r1_i, r2_i)
        min_j, max_j = min(r1_j, r2_j), max(r1_j, r2_j)

        for i in range(min_i, max_i+1):
            for j in range(min_j, max_j+1):
                if grid[i][j] == ' ':
                    return False
        return True

    # get the max area of a rectangle that is contained within the red-green polygon
    #   and has red tiles at two of its corners
    max_area = 0
    for r1, r2 in combinations(red_tiles, 2):
        area = get_area(r1, r2)
        if area <= max_area:
            # dont bother
            continue
        if not check_contained(r1, r2):
            continue
        max_area = area

    return max_area

sample = """7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3"""
assert part1(sample) == 50
assert part2(sample) == 24

___

[–] CameronDev@programming.dev 1 points 23 hours ago* (last edited 23 hours ago)

Rust

pt2: 71ms.

Wow. What a step up in difficulty. Using the geo library got me the right answer, and quickly in terms of code, but run time was terrible (2m+). Switching back to simply checking if a wall is entirely outside the rectangle got me a much faster win, and the code isn't too bad either.

Code and algorithm description(A wall is outside if its entirely to the left OR entirely to the right of the rect) OR (entirely above OR entirely below).

   fn check_wall_outside(
        c: &(&Coord, &Coord),
        top: usize,
        bottom: usize,
        left: usize,
        right: usize,
    ) -> bool {
        if (c.0.x <= left && c.1.x <= left) || (c.0.x >= right && c.1.x >= right) {
            return true;
        }
        if (c.0.y <= top && c.1.y <= top) || (c.0.y >= bottom && c.1.y >= bottom) {
            return true;
        }
        false
    }

   #[test]
    fn test_y2025_day9_part2_mine1() {
        let input = include_str!("../../input/2025/day_9.txt");
        let coords = input
            .lines()
            .map(|l| {
                let halfs = l.split_once(',').unwrap();
                Coord {
                    x: halfs.0.parse::<usize>().unwrap(),
                    y: halfs.1.parse::<usize>().unwrap(),
                }
            })
            .collect::<Vec<Coord>>();

        let mut walls = vec![];

        for i in 0..coords.len() {
            let first = &coords[i];
            let second = coords.get(i + 1).unwrap_or(coords.first().unwrap());

            walls.push((first, second));
        }

        let mut max_area = 0;
        for i in 0..coords.len() {
            let first = &coords[i];
            'next_rect: for j in i..coords.len() {
                let second = &coords[j];
                if first == second {
                    continue 'next_rect;
                }
                let area = (first.x.abs_diff(second.x) + 1) * (first.y.abs_diff(second.y) + 1);
                if area < max_area {
                    continue 'next_rect;
                }

                let (top, bottom) = if first.y > second.y {
                    (second.y, first.y)
                } else {
                    (first.y, second.y)
                };

                let (left, right) = if first.x > second.x {
                    (second.x, first.x)
                } else {
                    (first.x, second.x)
                };

                for wall in &walls {
                    if !check_wall_outside(wall, top, bottom, left, right) {
                        continue 'next_rect;
                    }
                }

                max_area = area;
            }
        }
        assert_eq!(max_area, 1542119040);
        println!("Part 2: {}", max_area);
    }

[–] VegOwOtenks@lemmy.world 3 points 1 day ago

Futhark

First, formatting the input with an unreadable sed script:

formatting script

1i [
1,$ {
	s/^/[/
	s/$/], /
}
$i ]
$d

Then, the actual program. main is the default entrypoint, part one is trivially solved in the preparations for part two. In part two, the faster check is to look for any point inside the current rectangle. If this can't find any, it'll have to check whether any edge crosses through the rectangle with a simple range check. I'm not happy with the performance, I feel like I left a lot on the table.

As always, wonky syntax highlighting

import "lib/github.com/diku-dk/sorts/radix_sort"

def (&&&) 'a 'b 'c (f: a -> b) (g: a -> c) (x: a): (b, c) = (f x, g x)
def odd (x: i64): bool = x % 2 == 1

def count 'a (f: a -> bool) (xs: []a): i64
  = map (f >-> i64.bool) xs |> reduce_comm (+) 0

def coordinateFromArray (as: [2]i64): (i64, i64)
  = (as[0], as[1])

def maximum = reduce_comm i64.max i64.lowest
def minimum = reduce_comm i64.min i64.highest

def concatMap [n] 'a 'b (f: a -> ?[l].[l]b) (placeholder: b) (xs: [n]a): *[]b
  = let totalLength = reduce (+) 0 <| map (\ x -> length (f x)) xs in
    ( loop (results, offset) = (replicate totalLength placeholder, 0)
      for x in xs
      do
        let bs = f x in
        let scatterIndices = indices bs |> map (+offset) in
        (scatter results scatterIndices bs, offset + length bs)
    ).0

def rectSize (a: (i64, i64)) (b: (i64, i64)) = 
  let dx = i64.max a.0 b.0 - i64.min a.0 b.0 in
  let dy = i64.max a.1 b.1 - i64.min a.1 b.1 in
  (dx + 1) * (dy + 1)

def pair_iota (n: i64): [n](i64, i64)
  = map (\ j -> (n, j)) (iota n)

def pairs 'a (xs: []a): [](a, a)
  = concatMap pair_iota (i64.highest, i64.highest) (indices xs)
    |> map (\ (i, j) -> (xs[i], xs[j]))

def findFirst 'a (f: a -> bool) (xs: []a): a
  = ( loop (i, x) = (0, xs[0])
      while not (f x)
      do (i + 1, xs[i+1])
    ) |> (.1)

def orderedPair (p: (i64, i64)): (i64, i64) = (i64.min p.0 p.1, i64.max p.0 p.1)

def overlapsWith (a: (i64, i64)) (b: (i64, i64)): bool 
  = a.0 < b.1 && b.0 < a.1

def anyInside (points: [](i64, i64)) (rectangle: (((i64, i64), (i64, i64)), i64))
  = let (lowerX, upperX) = orderedPair (rectangle.0.0.0, rectangle.0.1.0) in
    let (lowerY, upperY) = orderedPair (rectangle.0.0.1, rectangle.0.1.1) in
    map (\ (x, y) -> lowerX < x && x < upperX && lowerY < y && y < upperY) points
    |> or

def anyIntersects (edges: []((i64, i64), (i64, i64))) (rectangle: (((i64, i64), (i64, i64)), i64)): bool
  = let rectRangeX = orderedPair (rectangle.0.0.0, rectangle.0.1.0) in
    let rectRangeY = orderedPair (rectangle.0.0.1, rectangle.0.1.1) in
    map (\ e -> 
      let edgeRangeX = orderedPair (e.0.0, e.1.0) in
      let edgeRangeY = orderedPair (e.0.1, e.1.1) in
      (edgeRangeX `overlapsWith` rectRangeX) && (edgeRangeY `overlapsWith` rectRangeY)
    ) edges
    |> or

def part2 (sortedRectangles: [](((i64, i64), (i64, i64)), i64)) (points: [](i64, i64))
  = let edges = zip points (rotate 1 points) in
    let filled = \ r -> not (anyInside points r || anyIntersects edges r) in
    findFirst filled sortedRectangles
    |> (.1)

-- benchmark
-- ==
-- input @fut-input
-- auto output

def main (coordinateArrays: [][2]i64)
  = let coordinates = map coordinateFromArray coordinateArrays in
    let rectangleCorners = pairs coordinates in
    let rectangleSizes = map (id &&& uncurry rectSize) rectangleCorners in
    let sortedRectangles = radix_sort_by_key (.1) i64.num_bits i64.get_bit rectangleSizes |> reverse in
  (sortedRectangles[0].1, part2 sortedRectangles coordinates)

[–] Camille@lemmy.ml 3 points 1 day ago

Go

Oh my god. I'm calling: tomorrow will probably be the day I fail lmao. It has been so hard to get a correct result. Then I spent a solid half hour to optimize the solution so that :

  • it terminates
  • it doesn't need 600GB of RAM

It seems that using a int8 to encode color instead of the default int64 was the right idea lmao. I'm also happy to have understood how to multithread a Go application and I'm totally bought by the simplicity. This language really helped me by abstracting everything in goroutines and call it a day.

I have also been able to create small goroutines which job was to generate values one by one and abstract the real control-flow in order not to allocate a whole big array at once, reducing further my memory footprint.

This is the second time in my life I want to say I loved using Go. Last time was when I waited to pretty print json files, my colleague told me how to write a wannabe-oneliner in Go to do it myself. Never looked back.

day09.go

package main

import (
	"aoc/utils"
	"math"
	"slices"
	"strconv"
	"strings"
	"sync"
)

type point struct {
	x, y int
}

func (p point) areaUntil(other point) int {
	dx := other.x - p.x
	if dx < 0 {
		dx = -dx
	}
	dy := other.y - p.y
	if dy < 0 {
		dy = -dy
	}

	return (dx + 1) * (dy + 1)
}

func parsePoint(line string) point {
	parts := strings.Split(line, ",")
	x, _ := strconv.Atoi(parts[0])
	y, _ := strconv.Atoi(parts[1])
	return point{x, y}
}

func getPointChannel(input chan string) chan point {
	ch := make(chan point, cap(input))

	go func() {
		for line := range input {
			ch <- parsePoint(line)
		}
		close(ch)
	}()

	return ch
}

func stepOne(input chan string) (int, error) {
	points := []point{}
	maxArea := math.MinInt

	for p := range getPointChannel(input) {
		points = append(points, p)

		if len(points) == 1 {
			continue
		}

		areas := make([]int, len(points))
		for idx, p2 := range points {
			areas[idx] = p.areaUntil(p2)
		}

		max := slices.Max(areas)
		if max > maxArea {
			maxArea = max
		}
	}

	return maxArea, nil
}

type color int8

const (
	white color = iota
	green
	red
)

type matrix struct {
	tiles  [][]color
	points []point
}

func min(x, y int) int {
	if x < y {
		return x
	} else {
		return y
	}
}

func max(x, y int) int {
	if x > y {
		return x
	} else {
		return y
	}
}

func (m *matrix) squareContainsWhiteTiles(sq segment) bool {
	xMin := min(sq[0].x, sq[1].x)
	yMin := min(sq[0].y, sq[1].y)
	xMax := max(sq[0].x, sq[1].x)
	yMax := max(sq[0].y, sq[1].y)

	for y := yMin; y < yMax; y++ {
		if m.tiles[y][xMin] == white || m.tiles[y][xMax] == white {
			return true
		}
	}

	for x := xMin; x < xMax; x++ {
		if m.tiles[yMin][x] == white || m.tiles[yMax][x] == white {
			return true
		}
	}

	return false
}

func (m *matrix) addReds() {
	for _, p := range m.points {
		m.addRed(p)
	}
}

func newMatrix(points []point) (m matrix) {
	coords := getMatrixSize(points)
	rows := coords[1] + 1
	cols := coords[0] + 1
	array := make([][]color, rows)
	for idx := range array {
		array[idx] = make([]color, cols)
	}
	m = matrix{
		tiles:  array,
		points: points,
	}

	return m
}

func (m *matrix) addRed(p point) {
	m.tiles[p.y][p.x] = red
}

func (m *matrix) makeBorders() {
	for i := 0; i < len(m.points)-1; i++ {
		m.makeBorder(m.points[i], m.points[i+1])
	}
	m.makeBorder(m.points[len(m.points)-1], m.points[0])
}

func (m *matrix) makeBorder(p1 point, p2 point) {
	if p1.x == p2.x {
		m.makeVerticalBorder(p1.x, p1.y, p2.y)
	} else {
		m.makeHorizontalBorder(p1.x, p2.x, p1.y)
	}
}

func (m *matrix) makeHorizontalBorder(x1, x2, y int) {
	if x1 > x2 {
		tmp := x1
		x1 = x2
		x2 = tmp
	}

	for i := x1; i <= x2; i++ {
		if m.tiles[y][i] == white {
			m.tiles[y][i] = green
		}
	}
}

func (m *matrix) makeVerticalBorder(x, y1, y2 int) {
	if y1 > y2 {
		tmp := y1
		y1 = y2
		y2 = tmp
	}

	for i := y1; i <= y2; i++ {
		if m.tiles[i][x] == white {
			m.tiles[i][x] = green
		}
	}
}

func (m *matrix) makeGreens() {
	m.makeBorders()

	iterCount := len(m.tiles) / MagicThreadCount

	parallel(func(thrId int) {
		for i := range iterCount {
			row := m.tiles[iterCount*thrId+i]
			inside := false
			lastWasWhite := false

			for idx, cell := range row {
				switch cell {
				case white:
					if inside {
						row[idx] = green
					}
				default:
					if lastWasWhite {
						inside = !inside
					}
				}
				lastWasWhite = cell == white
			}
		}
	})
}

func getMatrixSize(points []point) [2]int {
	var x, y int
	for _, p := range points {
		if p.x > x {
			x = p.x
		}
		if p.y > y {
			y = p.y
		}
	}
	return [2]int{x, y}
}

func (m *matrix) String() string {
	iterCount := len(m.points) / MagicThreadCount
	lines := make([]string, len(m.tiles))

	parallel(func(thrId int) {
		for i := range iterCount {
			row := m.tiles[iterCount*thrId+i]
			runes := make([]rune, len(row))
			for idx, col := range row {
				switch col {
				case white:
					runes[idx] = '.'
				case green:
					runes[idx] = 'X'
				case red:
					runes[idx] = '#'
				}
			}
			lines[iterCount*thrId+i] = string(runes)
		}
	})

	return strings.Join(lines, "\n")
}

type segment [2]point

func newSegment(p1, p2 point) segment {
	seg := [2]point{p1, p2}
	return seg
}

func (m *matrix) allValidSquaresWith(p1 point) chan segment {
	ch := make(chan segment)

	go func() {
		for _, p2 := range m.points {
			seg := newSegment(p1, p2)
			if p2 != p1 {
				ch <- seg
			}
		}
		close(ch)
	}()

	return ch
}

// 495 == 55 * 9
// 495 == 99 * 5
// 495 == 165 * 3
var MagicThreadCount = 9

func parallel(f func(int)) {
	var wg sync.WaitGroup
	for thrId := range MagicThreadCount {
		wg.Go(func() {
			f(thrId)
		})
	}

	wg.Wait()
}

func stepTwo(input chan string) (int, error) {
	points := []point{}
	for p := range getPointChannel(input) {
		points = append(points, p)
	}
	m := newMatrix(points)
	m.addReds()
	m.makeGreens()

	iterCount := len(m.points) / MagicThreadCount
	boys := make([]int, MagicThreadCount)

	parallel(func(thrId int) {
		largestBoy := math.MinInt
		for i := range iterCount {
			p := m.points[iterCount*thrId+i]

			squareCh := m.allValidSquaresWith(p)
			for sq := range squareCh {
				area := sq[0].areaUntil(sq[1])
				if area > largestBoy && !m.squareContainsWhiteTiles(sq) {
					largestBoy = area
				}
			}
		}
		boys[thrId] = largestBoy
	})

	return slices.Max(boys), nil
}

func main() {
	input := utils.FilePath("day09.txt")
	utils.RunStep(utils.ONE, input, stepOne)
	utils.RunStep(utils.TWO, input, stepTwo)
}

[–] mykl@lemmy.world 5 points 1 day ago* (last edited 1 day ago)

Uiua

Part 1 was easy, part 2 is ...less so...

a hint that might help youvisualising the data reveals some interesting patterns. Follow link to do so.

Any way, here's my Part 1 while I'm still thinking about this.

# AOC 2025 Day 09
# Experimental!
D ← &fras"AOC2025day09.txt"  # Drop your file here and edit name
/β†₯/Γ—+1βŒ΅β‰/-⍉₂⧅<2β‹•Β°csv D       # Part 1 
βˆ§βœβŠ‘β‹…1⟜(Λœβ†―0+1/β†₯)βœβ‰β‰‘βœβ†βŠ›β‹•Β°csv D # Visualised data

Part 2

This is basically me thinking out loud, so it's untidy, brutal, repetitive and slow. (20s in the pad) link if you care

# Experimental!
# AOC 2025 Day 09
D     ← "7,1\n11,1\n11,7\n9,7\n9,5\n2,5\n2,3\n7,3"
D     ← &fras"AOC2025day09.txt" # Drop your file here and edit name
Parse ← β‹•Β°csv
Part₁ ← /β†₯/Γ—+1βŒ΅β‰/-⍉₂⧅<2

Squeeze ← βœβ‰β‰‘βœβ†βŠ›          # Squeeze (add a sort to inspect)
Draw    ← βˆ§βœβŠ‘β‹…1⟜(Λœβ†―0+1/β†₯) # Draw

HLines ← (
  Squeeze Parse D
  β†βŠ•(░⍆)βŠ›βŠΈβ‰‘βŠ’
  βˆ§β—‡(⍜⊑˜⍜(⊏|Λ™=)βŠ™Λœβˆ˜βŠƒ(⊒⊒|β†˜βŠ™β‡‘Β°βŠŸ+0_1βŠ£β‰))
)

VLines ← (
  Squeeze Parse D
  β†βŠ•(░⍆)βŠ›βŠΈβ‰‘βŠ£
  βˆ§β—‡(⍜⊑˜⍜(⊏|Λ™=)βŠ™Λœβˆ˜βŠƒ(⊣⊣|β†˜βŠ™β‡‘Β°βŠŸ+0_1βŠ’β‰))
)
DrawBorder ← βœβ‰VLines HLines Λœβ†―0β†―2+1/β†₯β™­Squeeze Parse D # Draws full border

# DrawBorder Squeeze # running this shows these as key boundary points -> [219 121] [219 123]
# βŠβ‰‘βŒŸΛœβ¨‚[[219 121] [219 123]]⊸Squeeze # which map to -> [[94985 48652][94985 50114]]
# leftmost -> only look left, rightmost-> only look right
# SO, now fill the shape to make this easier.
Fill ← (
  βŠ™βŠ™1           # fill colour.
  Good ← =0βŠ™β—ŒβŠ‘βŠ’ # Needs filling. Simple edges-check.
  # Take first of list. If needs fill, do so and then add
  # its four neighbours into queue. Repeat until queue is empty.
  ⍒(⨬(β†˜1|β—΄βŠ‚+Aβ‚‚Β€Β°βŠ‚ βŠƒ(β‹…βˆ˜|∘|β‹…β‹…β‹…βˆ˜)β—‘(⍜(⊑|β‹…βˆ˜)⊒))β—‘Good
  | >0β§»)
  β‹…βŠ™β‹…
)
DrawBorder     # Comment out everything below here to view the boundary.
Fill [219_120] # Now fill that boundary ([2_1] for test)
Squeeze Parse D
# ([0 1] for test)
[219 123] #  [219 121] gave the wrong answer, so it's this.
β‰‘βŒžβŠŸ       # couple this with every other point
# Extract the covered window of the array for each pair of corners.
# (Very probably doing this the hard way:-()
# Only keep those that are all 1.
β–½β€šβ‰‘βŒŸ(=1/Γ—β™­β‰‘βŒžβŠβŠ™βŠΛœβˆ˜βˆ©(β†˜βŠ™β‡‘Β°βŠŸ+0_1⍆)Β°βŠŸβ‰)
# Pick out our squeezed indices
⨂Squeeze Parse D
# Find out what the unsqueezed values were
˜⊏Parse D
# Find areas, return max.
/β†₯≑/Γ—+1βŒ΅β‰‘/-
[–] Zikeji@programming.dev 5 points 1 day ago (1 children)

Javascript

I generally like trying the brute force approach first (though in the latter days it just wastes time). After exhausting 32GB of RAM I changed approaches.

Admittedly I don't really understand these algorithms as well as I would like, and I also will admit I inadvertently was LLM assisted :(. When I was trying to figure out what algo I should use and the AI summary in Google's search spoiled me. But I did learn alot.

One interesting observation, unrelated to the solution itself, is the solutions runs about 30% faster in Firefox than it does via Node (25.2.1 and 24.11.1) on my machine.

Code

const input = require('fs').readFileSync('input-day9.txt', 'utf-8');

/** @type {Array<[number, number]} */
const points = input.split("\n").map(r => r.split(',').map(v => parseInt(v, 10)));

let largest = 0;
let largestInside = 0;
for (const [startX, startY] of points) {
    for (const [nextX, nextY] of points) {
        if (startX === nextX && startY === nextY) continue;

        const minX = Math.min(startX, nextX);
        const maxX = Math.max(startX, nextX);
        const minY = Math.min(startY, nextY);
        const maxY = Math.max(startY, nextY);

        const area = (maxX - minX + 1) * (maxY - minY + 1);
        if (area > largest) {
            largest = area;
        }

        if (area <= largestInside) continue;

        // center point check, ala https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule
        const centerX = (minX + maxX) / 2;
        const centerY = (minY + maxY) / 2;
        let inside = false;
        for (const [i, [aX, aY]] of points.entries()) {
            const [bX, bY] = points[i - 1] ?? points[points.length - 1];
            if (centerX === aX && centerY === aY) {
                inside = true;
                break;
            }
            if ((aY > centerY) !== (bY > centerY)) {
                const slope = (centerX - aX) * (bY - aY) - (bX - aX) * (centerY - aY);
                if (slope === 0) {
                    inside = true;
                    break;
                }
                if ((slope < 0) !== (bY < aY)) {
                    inside = !inside;
                }
            }
        }

        if (!inside) continue;

        // check for edge intersections, ala https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
        let intersects = false;
        for (const [i, [aX, aY]] of points.entries()) {
            const [bX, bY] = points[i - 1] ?? points[points.length - 1];

            if (aX === bX) {
                if (aX > minX && aX < maxX) {
                    const wallMinY = Math.min(aY, bY);
                    const wallMaxY = Math.max(aY, bY);

                    if (Math.max(minY, wallMinY) < Math.min(maxY, wallMaxY)) {
                        intersects = true;
                        break;
                    }
                }
            } else if (aY === bY) {
                if (aY > minY && aY < maxY) {
                    const wallMinX = Math.min(aX, bX);
                    const wallMaxX = Math.max(aX, bX);

                    if (Math.max(minX, wallMinX) < Math.min(maxX, wallMaxX)) {
                        intersects = true;
                        break;
                    }
                }
            }
        }

        if (intersects) continue;

        if (area > largestInside) {
            largestInside = area;
        }
    }
}

console.log(`Part 1 Answer: ${largest}`);
console.log(`Part 2 Answer: ${largestInside}`);

[–] Deebster@programming.dev 3 points 1 day ago

That is surprising about Firefox - you'd have thought something that literally just runs JavaScript would be able to beat Firefox with all the UI, sandboxing, etc. 30% is a huge margin.

[–] lwhjp@piefed.blahaj.zone 4 points 1 day ago* (last edited 1 day ago)

Haskell

This is pretty ugly. I got rather fed up after trying out various heuristics when the test case passed but actual data didn't.

import Control.Arrow  
import Data.Function  
import Data.Ix  
import Data.List  
import Data.Ord  

readInput :: String -> [(Int, Int)]  
readInput = map ((read *** (read . tail)) . break (== ',')) . lines  

pairs = concatMap (\(x : xs) -> map (x,) xs) . init . tails  

toRange ((a, b), (c, d)) = ((min a c, min b d), (max a c, max b d))  

onTiles loop rect = cornersInside && not crossingEdges  
  where  
    cornersInside =  
      let ((a, b), (c, d)) = rect  
       in inside (a, d) && inside (c, b)  
    verticalEdges = sortOn (Down . fst . fst) $ filter (uncurry ((==) `on` fst)) loop  
    inside (x, y) =  
      let intersecting ((a, b), (_, d)) = a <= x && inRange (min b d, max b d) y  
       in maybe False (uncurry ((>) `on` snd)) $ find intersecting verticalEdges  
    crossingEdges =  
      let ((a, b), (c, d)) = rect  
       in any (crossingLoop . toRange) $  
            [ ((a, b), (c, b)),  
              ((c, b), (c, d)),  
              ((c, d), (a, d)),  
              ((a, d), (a, b))  
            ]  
    crossingLoop ((a, b), (c, d))  
      | a == c = anyEdge (\((e, f), (g, h)) -> f == h && f > b && f < d && g > a && e < c)  
      | b == d = anyEdge (\((e, f), (g, h)) -> e == g && e > a && e < c && h > b && f < d)  
    anyEdge = flip any $ map toRange loop  

main = do  
  input <- readInput <$> readFile "input09"  
  let rects = pairs input  
      loop = zip (last input : input) input  
      go = print . maximum . map (rangeSize . toRange)  
  go rects  
  go $ filter (onTiles loop) rects  
[–] Avicenna@programming.dev 2 points 1 day ago* (last edited 1 day ago) (1 children)

Yes I used a polygon library so what... I did eyeball the shape and guessed what the result should have been like but still more pleasing to write something that applies generally. Although I did put an area threshold (eyeballed) which subsets the corners to test, it only reduces run time to about 1/2 (from ~10s to ~5s) so that is not necessarily very critical. If I hadn't used this library, I would probably do sth along the lines of defining my own rectangle classes with unions, intersections etc so wouldn't be a more clever approach but much more time consuming.

from pathlib import Path

import numpy as np
import shapely

cwd = Path(__file__).parent

def parse_input(file_path):
  with file_path.open("r") as fp:
    data = list(map(lambda x: list(map(int,x.split(','))), fp.readlines()))

  return np.array(data)


def construct_shapes(coordinates, threshold):

  Itriu = np.triu_indices(coordinates.shape[0], k=2)
  squares = []

  for i0,i1 in zip(*Itriu):

    c0 = tuple(coordinates[i0,:])
    c1 = tuple(coordinates[i1,:])
    area = np.prod(abs(np.array(c0) - np.array(c1)) + np.array([1,1]))

    if area>threshold:
      c2 = (c0[0],c1[1])
      c3 = (c1[0],c0[1])
      squares.append(shapely.Polygon((c0,c3,c1,c2)))

  polygon = shapely.Polygon(coordinates)

  return polygon, squares


def solve_problem(file_name, redgreen=False, threshold=0):

  coordinates = parse_input(Path(cwd, file_name))

  if not redgreen:
    areas = np.prod(abs(coordinates[None,:] - coordinates[:,None]) +\
                    np.array([1,1])[None,None,:], axis=-1)
    max_area = np.max(areas)

  else:
    polygon, squares = construct_shapes(coordinates, threshold)
    max_area = -np.inf

    for inds,square in enumerate(squares):
      if square.area==0:
        continue

      if polygon.contains(square):
        c = np.array(list(zip(*square.exterior.coords.xy)))
        if (a:=np.prod(abs(c[0] - c[2]) + np.array([1,1])))>max_area:
          max_area = a

  return int(max_area)



if __name__ == "__main__":

  assert solve_problem("test_input") == 50
  assert solve_problem("input") == 4763509452
  assert solve_problem("test_input", True, 1) == 24
  assert solve_problem("input", True, 15000*80000) == 1516897893 # threshold by eyeballing the shape

Even with using geo in rust, i am still struggling, so no shame in using the library. I did get the solve in 2m32s, but I dont feel like this is optimal.

Surely there is a better solution somewhere.