Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent 2024 - Day 12

day12.livemd

Advent 2024 - Day 12

Mix.install([
  {:kino, "~> 0.14.2"}
])

Setup

input = Kino.Input.textarea("Please paste your input file")
input = input
  |> Kino.Input.read()
  |> String.split("\n")
  |> Enum.map(&String.graphemes/1)
defmodule McMasterCarr do
  def get_surronding({x, y}) do
    [
      {x, y - 1},
      {x + 1, y},
      {x, y + 1},
      {x - 1, y}
    ]
  end

  def get_valid_surronding(board, seen, pos, val) do
    for {x, y} <- get_surronding(pos) do
      if x < 0 or y < 0 or x >= length(board) or y >= length(board) do
        nil
      else
        if board |> Enum.at(y) |> Enum.at(x) == val do
          if MapSet.member?(seen, {x, y}) do
            nil
          else
            {x, y}
          end
        else
          nil
        end
      end
    end
    |> Enum.filter(&amp;(&amp;1))
  end

  def cell_value(board, {x, y}) do
    if x < 0 or y < 0 or y >= length(board) or x >= board |> Enum.at(0) |> length do
      nil
    else
      board |> Enum.at(y) |> Enum.at(x)
    end
  end

  def find_chunks(board, pos, seen) do
    val = cell_value(board, pos)
    pieces = [pos]
    get_valid_surronding(board, seen, pos, val)
    |> Enum.reduce({pieces, seen}, fn pos, {pieces, seen} ->
      seen = MapSet.put(seen, pos)
      {new_pieces, seen} = find_chunks(board, pos, seen)
      pieces = pieces ++ new_pieces

      {pieces, seen}
    end)
  end

  def find_all_chunks(board) do
    positions = for x <- 0..(length(board) - 1) do
      for y <- 0..(length(board) - 1) do
        {x, y}
      end
    end
    |> List.flatten()

    {_seen, chunks} = Enum.reduce(positions, {MapSet.new(), []}, fn pos, {seen, chunks} ->
      if MapSet.member?(seen, pos) do
        {seen, chunks}
      else
        seen = MapSet.put(seen, pos)
        {pieces, seen} = find_chunks(board, pos, seen)
        chunks = [MapSet.new(pieces) | chunks]
        {seen, chunks}
      end
    end)

    chunks
  end
end

Part 1

McMasterCarr.find_all_chunks(input)
|> Enum.map(fn pieces ->
  fences = pieces
  |> Enum.map(fn piece ->
    McMasterCarr.get_surronding(piece)
    |> Enum.map(fn pos ->
      not MapSet.member?(pieces, pos)
    end)
    |> Enum.filter(&amp;(&amp;1))
    |> Enum.count()
  end)
  |> Enum.sum()

  MapSet.size(pieces) * fences
end)
|> Enum.sum()

Part 2

defmodule Grainger do
  def get_surronding({{x, y}, direction}) do
    if direction == 0 or direction == 2 do
      [
        {{x + 1, y}, direction},
        {{x - 1, y}, direction},
      ]
    else
      [
        {{x, y - 1}, direction},
        {{x, y + 1}, direction}
      ]
    end
  end

  def get_valid_surronding(fences, seen, fence) do
    for fence <- get_surronding(fence) do
      if MapSet.member?(seen, fence) or not MapSet.member?(fences, fence) do
        nil
      else
        fence
      end
    end
    |> Enum.filter(&amp;(&amp;1))
  end

  def find_chunks(fences, fence, seen) do
    pieces = MapSet.new([fence])
    get_valid_surronding(fences, seen, fence)
    |> Enum.reduce({pieces, seen}, fn fence, {pieces, seen} ->
      seen = MapSet.put(seen, fence)
      {new_pieces, seen} = find_chunks(fences, fence, seen)
      pieces = MapSet.union(pieces, new_pieces)

      {pieces, seen}
    end)
  end

  def combine_fences(fences) do
    {_seen, chunks} = Enum.reduce(fences, {MapSet.new(), []}, fn fence, {seen, chunks} ->
      if MapSet.member?(seen, fence) do
        {seen, chunks}
      else
        seen = MapSet.put(seen, fence)
        {pieces, seen} = find_chunks(fences, fence, seen)
        chunks = [pieces | chunks]
        {seen, chunks}
      end
    end)
    chunks
  end
end

McMasterCarr.find_all_chunks(input)
|> Enum.map(fn pieces ->
  fences = pieces
  |> Enum.map(fn piece ->
    McMasterCarr.get_surronding(piece)
    |> Enum.with_index()
    |> Enum.filter(fn {pos, _index} ->
      not MapSet.member?(pieces, pos)
    end)
  end)
  |> List.flatten()
  |> Enum.group_by(fn {_pos, direction} -> direction end)
  |> Enum.map(fn {_direction, fences} ->
    Grainger.combine_fences(fences |> MapSet.new())
    |> Enum.count()
  end)
  |> Enum.sum()

  MapSet.size(pieces) * fences
end)
|> Enum.sum()