lud

lud

Advent of Code 2024 - Day 12

At first I was scared but I found is a simple way to compute the sides.

defmodule AdventOfCode.Solutions.Y24.Day12 do
  alias AdventOfCode.Grid
  alias AoC.Input

  def parse(input, _part) do
    input |> Input.stream!() |> Grid.parse_lines(fn c -> {:ok, <<c>>} end) |> elem(0)
  end

  def part_one(full_grid) do
    regions = compute_regions(full_grid)

    regions
    |> Enum.map(&cost_p1/1)
    |> Enum.sum()
  end

  def part_two(full_grid) do
    regions = compute_regions(full_grid)

    regions
    |> Enum.map(&cost_p2/1)
    |> Enum.sum()
  end

  defp compute_regions(grid) do
    {regions, rest} =
      Enum.reduce(grid, {[], grid}, fn {pos, tag}, {regions, rest_grid} ->
        case Map.fetch(rest_grid, pos) do
          :error ->
            {regions, rest_grid}

          {:ok, _} ->
            {region, rest_grid} = take_region(rest_grid, tag, [pos])
            {[region | regions], rest_grid}
        end
      end)

    0 = map_size(rest)

    regions
  end

  defp take_region(mut_grid, tag, open, closed \\ [])

  defp take_region(mut_grid, tag, [pos | open], closed) do
    neighs = pos |> Grid.cardinal4() |> Enum.filter(fn xy -> xy not in closed && Map.get(mut_grid, xy) == tag end)
    take_region(mut_grid, tag, neighs ++ open, [pos | closed])
  end

  defp take_region(mut_grid, tag, [], closed) do
    region = Map.new(closed, &{&1, tag})

    mut_grid = Map.drop(mut_grid, closed)
    {region, mut_grid}
  end

  defp cost_p1(region) do
    area(region) * perimeter(region)
  end

  defp area(region) do
    map_size(region)
  end

  defp perimeter(region) do
    keys = Map.keys(region)

    Enum.reduce(region, 0, fn {xy, _}, acc ->
      borders = xy |> Grid.cardinal4() |> Enum.count(fn neigh -> neigh not in keys end)
      acc + borders
    end)
  end

  defp cost_p2(region) do
    area(region) * count_sides(region)
  end

  defp count_sides(region) do
    poses = Map.keys(region)

    individual_sides =
      Enum.flat_map(poses, fn pos ->
        [
          {:up, Grid.translate(pos, :n)},
          {:down, Grid.translate(pos, :s)},
          {:left, Grid.translate(pos, :w)},
          {:right, Grid.translate(pos, :e)}
        ]
        |> Enum.filter(fn {_, xy} -> xy not in poses end)
      end)

    sides_by_direction =
      Enum.group_by(
        individual_sides,
        fn
          # group sides by their orientation and level
          {:up, {_x, y}} -> {:up, y}
          {:down, {_x, y}} -> {:down, y}
          {:right, {x, _y}} -> {:right, x}
          {:left, {x, _y}} -> {:left, x}
        end,
        fn
          # keep side value by orientation and cross direction to know if their
          # are touching
          {:up, {x, _y}} -> x
          {:down, {x, _y}} -> x
          {:right, {_x, y}} -> y
          {:left, {_x, y}} -> y
        end
      )

    Enum.reduce(sides_by_direction, 0, fn {{_direction, _level}, cross_coords}, acc ->
      distinct_sides(cross_coords) + acc
    end)
  end

  defp distinct_sides(cross_coords) do
    [h | cross_coords] = Enum.sort(cross_coords)
    distinct_sides(cross_coords, h, 0)
  end

  defp distinct_sides([h | t], prev, acc) when h == prev + 1 do
    # No need to accumulate the whole side cross coordinates, we can just keep
    # the previous nuber
    distinct_sides(t, h, acc)
  end

  defp distinct_sides([h | t], _prev, acc) do
    distinct_sides(t, h, acc + 1)
  end

  defp distinct_sides([], _prev, acc) do
    acc + 1
  end
end

BEAM capability of guards like when h == prev + 1 is really neat.

Most Liked

rvnash

rvnash

My solution solves parts 1 and 2 simultaneously. For part 2 I just count the number of corners, both inside and outside. That’s equivalent to the number of sides.

AOC 2024 Day 12 Content 0
Part 1: 1549354 in 43.439ms
Part 2: 937032 in 43.241ms
sevenseacat

sevenseacat

Author of Ash Framework

This was a fun one! Took a bit of thinking outside the box (or maybe I didn’t need to think outside the box, I haven’t looked at anyone else’s solutions yet, I’ll do that now!)

The core of mine is the group_connecting function, which takes a list of coordinates and groups them together if they’re adjacent. This is how I figure out where all the regions are, and also how I split up all the borders into sides.

Name                     ips        average  deviation         median         99th %
day 12, part 1          7.16      139.70 ms     ±1.82%      139.22 ms      144.95 ms
day 12, part 2          6.84      146.22 ms     ±1.32%      145.82 ms      151.25 ms
liamcmitchell

liamcmitchell

I appreciate the solutions others post, it’s helped me learn a lot :heart:

I used a recursive function to build a set of region positions, then iterated over individual fences, building up a map of %{fenceStart => fenceEnd, fenceEnd => fenceStart}.

Pattern matching on maps is really clean.

adamu

adamu

:exploding_head:

billylanchantin

billylanchantin

This one is utterly unreadable after all the golfing.

LOC: 37

defmodule Aoc2024.Day12 do
  import Enum
  import String, only: [split: 3]
  def part1(file), do: main(file, &perimeter/1)
  def part2(file), do: main(file, &num_sides/1)
  def area(region), do: length(region)
  def perimeter(region), do: count(segment_counts(region), fn {_, count} -> count == 1 end)
  def segment_counts(region), do: frequencies(flat_map(region, &s/1))
  def s(p), do: for(q <- [p], r <- g(p), do: {q, r}) ++ for(q <- g(p), r <- [inc(p)], do: {q, r})
  def inc({x, y}), do: {x + 1, y + 1}
  def g({x, y}), do: [{x + 1, y}, {x, y + 1}]
  def t1?({a, b}, {c, d}), do: (a == c and abs(d - b) == 1) or (b == d and abs(c - a) == 1)
  def t2?(meets), do: fn {a, b}, {c, d} -> t3?(a, d, meets) or t3?(b, c, meets) end
  def t3?(p, q, meets), do: p == q and not (p in meets or q in meets)

  def main(file, side_fun) do
    rows = file |> File.read!() |> split("\n", trim: true) |> map(&split(&1, "", trim: true))
    grid = for {row, i} <- with_index(rows), {x, j} <- with_index(row), into: %{}, do: {{i, j}, x}
    rs = for {_, r} <- group_by(grid, &elem(&1, 1), &elem(&1, 0)), do: contiguous(r, &t1?/2)
    sum_by(rs, fn lr -> sum_by(lr, &(area(&1) * side_fun.(&1))) end)
  end

  def contiguous([h | t], fun?) do
    reduce(t, [[h]], fn x, ys ->
      {touches, disjoint} = split_with(ys, fn y -> any?(y, &fun?.(&1, x)) end)
      [[x] ++ List.flatten(touches)] ++ disjoint
    end)
  end

  def num_sides(region) do
    s = segment_counts(region) |> filter(fn {_, count} -> count == 1 end) |> map(&elem(&1, 0))
    %{true: v, false: h} = group_by(s, fn {{x1, _}, {x2, _}} -> x1 == x2 end)
    p_counts = (v ++ h) |> flat_map(fn {{a, b}, {c, d}} -> [{a, b}, {c, d}] end) |> frequencies()
    meets = p_counts |> filter(fn {_, count} -> count > 2 end) |> map(&elem(&1, 0))
    count(contiguous(v, t2?(meets))) + count(contiguous(h, t2?(meets)))
  end
end

Where Next?

Popular in Challenges Top

LostKobrakai
This topic is about Day 9 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
JEG2
Note: This topic is to talk about Day 9 of the Advent of Code. For general discussion about the Advent of Code 2018 and links to topics ...
New
antoine-duchenet
Everything went smoothly today. Nothing to change to solve part 2 because I already used memoization for part 1 (it looked like an AoC e...
New
kwando
Phew, this one took a while to get right. My naive attempts was way to slow so I reached for Dijkstras shortest path algorithm… and that ...
New
adamu
I said I was on a break, but I took a sneak peak and it looked fun so… Part 1 completes in half a millisecond with a single pass of the ...
New
bjorng
Note: This topic is to talk about Day 13 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
christhekeele
Setting this down for the night, as after a quick naive solve for quick part 1 I realize that part 2 is by design computationally expensi...
New
bjorng
Here is my solution for day 1 of Advent of Code: defmodule Day01 do def part1(input) do all = parse(input) {first, second} = E...
New
igorb
Today is a brute-force day: advent-of-code-2024/lib/advent_of_code2024/day6.ex at main · ibarakaiev/advent-of-code-2024 · GitHub Takes a...
New
cblavier
Hi, there :wave: Today, I felt it was way more challenging! I went through part2 thanks to Agent based memoization (without memoization ...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement