From 7aba3bab17b9fc5b11a5f7f83b10455a11643b25 Mon Sep 17 00:00:00 2001 From: Caleb Webber Date: Thu, 12 Dec 2024 14:43:30 -0500 Subject: [PATCH] add 2024 day 12 --- elixir/livebook/2024/day12.livemd | 271 ++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 elixir/livebook/2024/day12.livemd diff --git a/elixir/livebook/2024/day12.livemd b/elixir/livebook/2024/day12.livemd new file mode 100644 index 0000000..1c5569b --- /dev/null +++ b/elixir/livebook/2024/day12.livemd @@ -0,0 +1,271 @@ +# 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) +```