[–] 3 points 11 months 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")
    }
}
  • source
  • [–] 4 points 11 months ago* (last edited 11 months ago) (3 children)

    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);
    }
    
  • source
  • [–] 3 points 11 months 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)
    

  • source
  • [–] 2 points 11 months ago

    Squeezing all spaces out of the grid (and plotting at right-angles to the other visualisation here) gives this evil shape, which will doubtless haunt my dreams tonight.

  • source
  • [–] 4 points 11 months 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)
    }
    

  • source
  • [–] 2 points 11 months ago (3 children)

    I can't code today because of travel, just as the one that might run slow enough to geek out on benchmarking the various options. Does seem like it's a bit of a jump in difficulty from previous days.

  • source
  • [–] 2 points 11 months ago* (3 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
    
  • source
  • [–] 5 points 11 months ago* (last edited 11 months ago) (2 children)

    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βŒ΅β‰‘/-
    
  • source
  • [–] 4 points 11 months ago* (last edited 11 months 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  
    
  • source
  • [–] 5 points 11 months ago (1 child)

    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}`);
    

  • source
  • [–] 2 points 11 months ago (1 child)

    Correction, turns out doing it entirely in Excel was the answer for me. The points I found were correct, but the area calculation in my code was wrong.

    I had to visualize it before I could even attempt to solve it. Still did it mostly intuitively based on the visualization.

  • source
  • parent
  • context
  • [–] [S] 2 points 11 months ago (1 child)

    Thanks for your thoughts! For the input data, I did notice that every substitution goes from 1 element to 2-8 elements. I suppose I could take that into account, and order them from "most efficient" to "least efficient", and that way I might be able to stop iterating earlier?

    I also haven't looked into whether there are any combinations that are impossible to build from (or inversely to break down into e).

    Those two are probably good avenues to try.

    Thanks again!

  • source
  • parent
  • context
  • [–] 2 points 11 months ago* (2 children)

    Oh gosh, I remember this one. Working backwards is a good idea. In addition, you can just look at the start of the string when trying substitutions. I don't think that's valid in general, but it worked for me in this case.

    There's another trick you can do if you look carefully at the input data. I didn't implement it in my solution because I didn't spot it myself, but it essentially makes the problem trivial.

  • source
  • [–] 3 points 11 months ago

    Uiua

    Heavily copied ahem, inspired by @mykl@lemmy.world's solution :)

    Parse     ← Β°βŠ‚ β–½βŠΈβ‰‘/β†₯β‰ @.βŠœβˆ˜βŠΈβ‰ @\n
    Flow      ← +βŠƒ(/+≑↻1_Β―1€×|Γ—βŠ™Β¬)
    Propagate ← ˜∧(⊸˜Flow)
    Part₁     ← /+♭↧ ⊸Propagate
    Partβ‚‚     ← /+⊣Propagate βŠ™(ΛœβŠ‚0)
    
    $ .......S.......
    $ ...............
    $ .......^.......
    $ ...............
    $ ......^.^......
    $ ...............
    $ .....^.^.^.....
    $ ...............
    $ ....^.^...^....
    $ ...............
    $ ...^.^...^.^...
    $ ...............
    $ ..^...^.....^..
    $ ...............
    $ .^.^.^.^.^...^.
    $ ...............
    &fras "7.txt"
    
    βŠƒ(Part₁|Partβ‚‚) Parse
    
  • source
  • view more: β€Ή prev next β€Ί