This repository has been archived by the owner on Jan 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathday_09.ex
86 lines (69 loc) · 2.96 KB
/
day_09.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
defmodule AdventOfCode.Day09 do
import AdventOfCode.Utils
@typep heightmap :: [[integer()]]
@typep coordinates :: {integer(), integer()}
@spec part1([binary()]) :: integer()
def part1(args) do
heightmap = parse_args(args)
heightmap
|> all_coordinates()
|> Enum.filter(&low_point?(&1, heightmap))
|> Enum.map(&heightmap_at(&1, heightmap))
|> Enum.map(&(&1 + 1))
|> Enum.sum()
end
@spec part2([binary()]) :: integer()
def part2(args) do
heightmap = parse_args(args)
all_coordinates(heightmap)
|> Enum.filter(&low_point?(&1, heightmap))
|> Enum.map(&basin_size(&1, heightmap))
|> Enum.sort(:desc)
|> Enum.take(3)
|> Enum.product()
end
@spec all_coordinates(heightmap()) :: [coordinates()]
defp all_coordinates(map) do
Enum.flat_map(Enum.with_index(map), fn {line, y} ->
Enum.map(Enum.with_index(line), fn {_, x} ->
{x, y}
end)
end)
end
# Iteratively find all coordinates belonging to a basin from a starting point
@spec basin_size(coordinates(), heightmap()) :: integer()
defp basin_size(coords, heightmap), do: fill_basin([coords], MapSet.new(), heightmap)
@spec fill_basin([coordinates()], MapSet.t(coordinates()), heightmap()) :: integer()
defp fill_basin([], marked, _), do: MapSet.size(marked)
defp fill_basin([coords | queue], marked, heightmap) do
unmarked_adjacent =
adjacent_coordinates(coords)
|> Enum.filter(&(!MapSet.member?(marked, &1)))
|> Enum.filter(&in_basin?(heightmap_at(&1, heightmap)))
fill_basin(unmarked_adjacent ++ queue, MapSet.put(marked, coords), heightmap)
end
@spec low_point?(coordinates(), heightmap()) :: boolean()
defp low_point?(coords, heightmap) do
adjacent_heights(coords, heightmap)
|> Enum.map(&is_lower?(heightmap_at(coords, heightmap), &1))
|> Enum.all?()
end
@spec adjacent_heights(coordinates(), heightmap()) :: [integer()]
defp adjacent_heights(coords, heightmap) do
adjacent_coordinates(coords) |> Enum.map(&heightmap_at(&1, heightmap))
end
@spec adjacent_coordinates(coordinates()) :: [coordinates()]
defp adjacent_coordinates({x, y}), do: [{x, y - 1}, {x - 1, y}, {x + 1, y}, {x, y + 1}]
@spec is_lower?(integer(), integer() | nil) :: boolean()
defp is_lower?(element, adjacent), do: adjacent == nil or element < adjacent
@spec in_basin?(integer() | nil) :: boolean()
defp in_basin?(height), do: height != nil and height < 9
# Retrieve the element at the specified coordinates, or return nil if out of bounds
@spec heightmap_at(coordinates(), heightmap()) :: integer() | nil
defp heightmap_at({x, y}, _) when x == -1 or y == -1, do: nil
defp heightmap_at({x, y}, heightmap), do: heightmap |> Enum.at(y, []) |> Enum.at(x)
# Create a two-dimensional integer heightmap from the input
@spec parse_args([binary()]) :: [[integer()]]
defp parse_args(args), do: Enum.map(args, &parse_line/1)
defp parse_line(line), do: String.graphemes(line) |> Enum.map(&parse_int!/1)
end