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

Day 12: Garden Groups

day12.livemd

Day 12: Garden Groups

Mix.install([:kino])

Section

input = Kino.Input.textarea("input")
input = Kino.Input.read(input)

Part 1 & 2

defmodule GardenFencing do
  def solve(input) do
    grid = parse_input(input)
    dimensions = get_dimensions(grid)

    # part 1
    grid
    |> find_regions(dimensions)
    |> Enum.map(&calculate_region_price/1)
    |> Enum.sum()
    |> IO.inspect()

    # part 2
    grid
    |> find_regions(dimensions)
    |> Enum.map(&calculate_region_discount_price/1)
    |> Enum.sum()
    |> IO.inspect()
  end

  defp parse_input(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {line, y}, acc ->
      line
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce(acc, fn {char, x}, acc2 ->
        Map.put(acc2, {x, y}, char)
      end)
    end)
  end

  defp get_dimensions(grid) do
    {{max_x, _}, _} = Enum.max_by(grid, fn {{x, _}, _} -> x end)
    {{_, max_y}, _} = Enum.max_by(grid, fn {{_, y}, _} -> y end)
    {max_x + 1, max_y + 1}
  end

  defp find_regions(grid, dimensions) do
    grid
    |> Map.keys()
    |> Enum.reduce({[], MapSet.new()}, fn coord, {regions, visited} ->
      if MapSet.member?(visited, coord) do
        {regions, visited}
      else
        region = flood_fill(grid, dimensions, coord, MapSet.new())
        {[region | regions], MapSet.union(visited, region)}
      end
    end)
    |> elem(0)
  end

  defp flood_fill(grid, dimensions, {x, y} = coord, visited) do
    if MapSet.member?(visited, coord) or
         !Map.has_key?(grid, coord) or
         x < 0 or y < 0 or
         x >= elem(dimensions, 0) or
         y >= elem(dimensions, 1) do
      visited
    else
      current_type = Map.get(grid, coord)
      visited = MapSet.put(visited, coord)

      neighbors = neighbors({x, y}, false)

      Enum.reduce(neighbors, visited, fn neighbor, acc ->
        if Map.get(grid, neighbor) == current_type do
          flood_fill(grid, dimensions, neighbor, acc)
        else
          acc
        end
      end)
    end
  end

  defp calculate_region_price(region) do
    area = MapSet.size(region)
    perimeter = calculate_perimeter(region)
    area * perimeter
  end

  defp neighbors({x, y}, false) do
    [
      {x + 1, y},
      {x - 1, y},
      {x, y + 1},
      {x, y - 1}
    ]
  end

  defp neighbors({x, y}, true) do
    neighbors({x, y}, false) ++
      [
        {x + 1, y + 1},
        {x - 1, y + 1},
        {x - 1, y - 1},
        {x + 1, y - 1}
      ]
  end

  defp get_perimeters_with_diagonal(region) do
    Enum.reduce(region, [], fn {x, y}, acc ->
      neighbors = neighbors({x, y}, true)

      if Enum.any?(neighbors, fn coord -> !MapSet.member?(region, coord) end) do
        [{x, y} | acc]
      else
        acc
      end
    end)
  end

  defp is_corner?(region, {x, y}, {dx1, dy1}, {dx2, dy2}) do
    !MapSet.member?(region, {x + dx1, y + dy1}) &amp;&amp;
      !MapSet.member?(region, {x + dx2, y + dy2})
  end

  defp is_diagonal_corner?(region, {x, y}, {dx1, dy1}, {dx2, dy2}, {dx3, dy3}) do
    !MapSet.member?(region, {x + dx1, y + dy1}) &amp;&amp;
      MapSet.member?(region, {x + dx2, y + dy2}) &amp;&amp;
      MapSet.member?(region, {x + dx3, y + dy3})
  end

  defp get_corners(region, {x, y}) do
    out_corners_coord = [
      # top-left
      [{-1, 0}, {0, -1}],
      # top-right
      [{1, 0}, {0, -1}],
      # bottom-left
      [{-1, 0}, {0, 1}],
      # bottom-right
      [{1, 0}, {0, 1}]
    ]

    ins_corners_coord = [
      # top-left
      [{-1, -1}, {-1, 0}, {0, -1}],
      # top-right
      [{1, -1}, {0, -1}, {1, 0}],
      # bottom-left
      [{-1, 1}, {-1, 0}, {0, 1}],
      # bottom-right
      [{1, 1}, {1, 0}, {0, 1}]
    ]

    Enum.reduce(out_corners_coord ++ ins_corners_coord, 0, fn
      [{dx1, dy1}, {dx2, dy2}], acc ->
        if is_corner?(region, {x, y}, {dx1, dy1}, {dx2, dy2}) do
          acc + 1
        else
          acc
        end

      [{dx1, dy1}, {dx2, dy2}, {dx3, dy3}], acc ->
        if is_diagonal_corner?(region, {x, y}, {dx1, dy1}, {dx2, dy2}, {dx3, dy3}) do
          acc + 1
        else
          acc
        end
    end)
  end

  defp calculate_region_discount_price(region) do
    area = MapSet.size(region)
    sides = calculate_sides(region)
    area * sides
  end

  defp calculate_sides(region) do
    Enum.map(get_perimeters_with_diagonal(region), fn {x, y} ->
      get_corners(region, {x, y})
    end)
    |> Enum.sum()
  end

  defp calculate_perimeter(region) do
    Enum.reduce(region, 0, fn {x, y}, acc ->
      neighbors = neighbors({x, y}, false)

      exposed_sides = Enum.count(neighbors, fn coord -> !MapSet.member?(region, coord) end)
      acc + exposed_sides
    end)
  end
end

GardenFencing.solve(input)
1396298
853588