# AOC2024 Day 12 ## Section ```elixir defmodule Grid2D do defstruct width: nil, height: nil @directions %{ straight: [ {1, 0}, {-1, 0}, {0, 1}, {0, -1} ], diagonal: [ {1, 1}, {1, -1}, {-1, 1}, {-1, -1} ] } def new(width, height) do %Grid2D{width: width, height: height} end @doc """ ## Examples iex> Grid2D.index_to_coord(Grid2D.new(5,5), 0) {0,0} iex> Grid2D.index_to_coord(Grid2D.new(5,5), 5) {0,1} iex> Grid2D.index_to_coord(Grid2D.new(5,5), 6) {1,1} iex> Grid2D.index_to_coord(Grid2D.new(5,5), 3) {3,0} iex> Grid2D.index_to_coord(Grid2D.new(5,5), 24) {4, 4} """ def index_to_coord(%Grid2D{width: w}, index) do {rem(index, w), floor(index / w)} end @doc """ ## Examples iex> Grid2D.coord_to_index( Grid2D.new(5,5), {0,0}) 0 iex> Grid2D.coord_to_index( Grid2D.new(5,5), {4,4}) 24 iex> Grid2D.coord_to_index( Grid2D.new(5,5), {2,2}) 12 """ def coord_to_index(%Grid2D{width: w}, {x, y}) do y * w + x end @doc """ ## Examples iex> Grid2D.neighbors({0, 0}, Grid2D.new(5,5)) |> MapSet.new() MapSet.new([{0, 1}, {1, 1}, {1,0}]) iex> Grid2D.neighbors({2, 2}, Grid2D.new(5,5)) |> MapSet.new() [{1, 1}, {1, 2}, {1, 3}, {2, 1}, {2, 3}, {3, 1}, {3, 2}, {3, 3}] |> MapSet.new() iex> Grid2D.neighbors({4, 4}, Grid2D.new(5,5)) |> MapSet.new [{3, 3}, {3, 4}, {4, 3}] |> MapSet.new """ def neighbors(p, g, allowed_directions \\ :all) do directions(allowed_directions) |> Stream.map(&add(p, &1)) |> Stream.filter(&is_in_grid?(&1, g)) end @doc """ Distance from two points in a grid iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {0,1}, :radial) 1 iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,1}, :radial) 1 iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,2}, :radial) 2 """ def distance(grid, p1, p2, type) def distance(_, {x, y}, {a, b}, :radial), do: max(abs(x - a), abs(y - b)) @doc """ Add two points together pairwise iex> Grid2D.add({0,0}, {0,1}) {0,1} iex> Grid2D.add({2,3}, {3,2}) {5,5} """ def add({x, y}, {j, k}), do: {x + j, y + k} @doc """ Test if a point is in a grid iex> Grid2D.is_in_grid?({1, 2}, %Grid2D{width: 3, height: 4}) true iex> Grid2D.is_in_grid?({2, 2}, %Grid2D{width: 2, height: 4}) false """ def is_in_grid?({x, y}, %Grid2D{width: w, height: h}), do: x < w and y < h and x >= 0 and y >= 0 @spec directions(allowed :: :all | :straight | :diagonal) :: any() def directions(allowed \\ :all) def directions(:all), do: directions(:straight) ++ directions(:diagonal) def directions(:straight), do: @directions |> Map.get(:straight) def directions(:diagonal), do: @directions |> Map.get(:diagonal) def mul({x, y}, n), do: {n*x, n*y} end ``` ```elixir defmodule EnumUtil do def pick_n(enum, n) def pick_n([], _), do: [] def pick_n([a | rest], n) do pick_n(rest, n, n, []) ++ pick_n(rest, n, n - 1, [a]) end defp pick_n([], n, _, acc) do if acc |> Enum.count == n do [acc] else [] end end defp pick_n(_, _, 0, acc), do: [acc] defp pick_n([a | rest], n, r, acc) do pick_n(rest, n, r, acc) ++ pick_n(rest, n, r - 1, [a | acc]) end end ``` ```elixir input = """ """ ``` ```elixir lines = input |> String.trim() |> String.split("\n") height = lines |> Enum.count width = lines |> Enum.at(0) |> String.length() tiles = lines |> Stream.with_index() |> Stream.flat_map(fn {line, i} -> line |> String.graphemes() |> Stream.with_index() |> Enum.map(fn {c, j} -> {{j, i}, c} end) end) |> Enum.into(%{}) grid = Grid2D.new(width, height) ``` ```elixir defmodule Crawl do def crawl([], _, _, acc), do: acc def crawl(points, tiles, grid, acc) do n = points |> Enum.flat_map(fn p -> Grid2D.neighbors(p, grid, :straight) |> Enum.reject(&(MapSet.member?(acc, &1) or Map.get(tiles, &1) != Map.get(tiles, p))) end) |> Enum.uniq() # IO.puts("#{inspect(n)}") crawl(n, tiles, grid, points |> Enum.reduce(acc, fn x, s -> MapSet.put(s, x) end)) end def crawl(tiles, grid) do for {pt, _} <- tiles, reduce: {MapSet.new([]),[]} do {seen, list} -> if !MapSet.member?(seen, pt) do visited = Crawl.crawl([pt], tiles, grid, MapSet.new([pt])) {seen |> MapSet.union(visited), [visited | list]} else {seen, list} end end |> elem(1) end def pricep1(regions) do for region <- regions, reduce: 0 do acc -> area = region |> MapSet.size() perimeter = 4*area - (region |> MapSet.to_list() |> EnumUtil.pick_n(2) |> Stream.map( fn [{a,b},{x,y}] -> if (a == x and abs(b-y) == 1) or (b == y and abs(a-x) == 1) do 2 else 0 end end ) |> Enum.sum()) acc + area*perimeter end end def rotate90({a, b}), do: {-b, a} def pricep2(regions, tiles, grid) do for region <- regions, reduce: 0 do acc -> sides = for direction <- Grid2D.directions(:straight) do for tile <- region, neighbor = Grid2D.add(tile, direction), Map.get(tiles, neighbor) != Map.get(tiles, tile), reduce: {MapSet.new, 0} do {used, total} -> if !MapSet.member?(used, tile) do scan_dir = rotate90(direction) line = Stream.iterate(1, &(&1 + 1)) |> Stream.map(fn n -> if rem(n, 2) == 0 do {:up, div(n,2)} else {:down, div(n+1, 2)*-1} end end) |> Stream.map(&({elem(&1, 0), Grid2D.mul(scan_dir, elem(&1, 1))})) |> Enum.reduce_while( %{line: MapSet.new, up: true, down: true}, fn {dir, next}, %{line: line} = acc -> next = Grid2D.add(next, tile) is_same? = Map.get(tiles, next) == Map.get(tiles, tile) next_external? = Map.get(tiles, Grid2D.add(next, direction)) != Map.get(tiles, next) acc = if acc[dir] and Grid2D.is_in_grid?(next, grid) and is_same? and next_external? do %{acc | line: MapSet.put(line, next)} else acc |> Map.put(dir, false) end {(if acc[:up] or acc[:down], do: :cont, else: :halt), acc} end ) |> Map.get(:line) {used |> MapSet.union(line), total + 1} else {used, total} end end |> elem(1) end |> Enum.sum() area = region |> MapSet.size() acc + area*sides end end end ``` ```elixir regions = Crawl.crawl(tiles, grid) ``` ```elixir Crawl.pricep1(regions) ``` ```elixir Crawl.pricep2(regions, tiles, grid) ```