LostKobrakai
Advent of Code 2022 - Day 8
This one has been quite the ride. Struggled at first to find a good data format to suite the problem. I really like how that turned out by separating the map data from coordinates to look at when counting. Part 2 also was imo not well defined. It took me a while to figure out I don’t need to subtract smaller trees behind larger trees anymore.
Solution
defmodule Day8 do
defstruct map: nil, size: nil
def parse(text) do
lines = text |> String.split("\n") |> Enum.reject(&(&1 == ""))
{map, _} =
lines
|> Enum.with_index()
|> Enum.flat_map_reduce(0, fn {line, y}, next ->
line
|> String.split("", trim: true)
|> Enum.with_index()
|> Enum.map_reduce(next, fn {height, x}, next ->
item = {{x, y}, %{id: next, height: String.to_integer(height)}}
{item, next + 1}
end)
end)
map = Map.new(map)
keys = Map.keys(map)
size_x = keys |> Enum.map(fn {x, _} -> x end) |> Enum.max()
size_y = keys |> Enum.map(fn {_, y} -> y end) |> Enum.max()
%__MODULE__{map: map, size: %{x: size_x, y: size_y}}
end
def count_visible_from_outside(text) do
data = parse(text)
from_left_keys =
for y <- 0..data.size.y//1 do
for x <- 0..data.size.x//1, do: {x, y}
end
from_right_keys =
for y <- 0..data.size.y//1 do
for x <- data.size.x..0//-1, do: {x, y}
end
from_top_keys =
for x <- 0..data.size.x//1 do
for y <- 0..data.size.y//1, do: {x, y}
end
from_bottom_keys =
for x <- 0..data.size.x//1 do
for y <- data.size.y..0//-1, do: {x, y}
end
[
from_left_keys,
from_right_keys,
from_top_keys,
from_bottom_keys
]
|> Enum.flat_map(& &1)
|> Enum.flat_map(&count_visible_line(data.map, &1))
|> Enum.uniq()
|> Enum.count()
end
defp count_visible_line(map, line) do
{_, trees} =
line
|> Enum.map(fn coordinate -> Map.fetch!(map, coordinate) end)
|> Enum.reduce({-1, []}, fn
%{height: tree_height} = tree, {line_of_sight, visible}
when tree_height > line_of_sight ->
{tree_height, [tree | visible]}
_, acc ->
acc
end)
trees
end
def count_visible_from_tree_house(text) do
data = parse(text)
for y <- 0..data.size.y//1, x <- 0..data.size.x//1 do
tree = Map.fetch!(data.map, {x, y})
to_left = for x <- (x - 1)..0//-1, do: {x, y}
to_right = for x <- (x + 1)..data.size.x//1, do: {x, y}
to_top = for y <- (y - 1)..0//-1, do: {x, y}
to_bottom = for y <- (y + 1)..data.size.y//1, do: {x, y}
[
to_top,
to_left,
to_right,
to_bottom
]
|> Enum.map(fn line ->
data.map |> count_visible_line_treehouse(line, tree.height)
end)
|> Enum.reduce(&Kernel.*/2)
end
|> Enum.max()
end
defp count_visible_line_treehouse(map, line, limit) do
line
|> Enum.map(fn coordinate -> Map.fetch!(map, coordinate) end)
|> Enum.reduce_while(0, fn
tree, num when tree.height >= limit -> {:halt, num + 1}
_, num -> {:cont, num + 1}
end)
end
end
Most Liked
deadbeef
Late to the party. Brute forced like others
https://github.com/ed-flanagan/advent-of-code-solutions-elixir/blob/main/lib/advent/y2022/d08.ex
al2o3cr
A tiny bit of code review on the above - nothing major, mostly “here’s a shorter way to write the same ideas” tips.
-
many
Enumfunctions have a variant that lets you transform the input before doing their thing. For instance,Enum.count/2orEnum.max_by/4. There’s a minor performance benefit of using them since the intermediate list doesn’t need to be constructed, but IMO the readability gain is better. -
Enum.map+List.flatten==Enum.flat_map, only again the intermediate list doesn’t need to be constructed. -
Most code that uses
Enum.reducewith an initial value of%{}will be clearer withMap.new. I say “most” because sometimes there’s code in the block passed toreducethat returnsaccunchanged, which you can’t do withMap.new -
most of the time when you want one-line-at-a-time,
File.stream!will save you some typing. By default, it already splits lines. There is also a theoretical memory-usage advantage since usingStreammeans you don’t need every line in memory at once, but it’s unlikely to be important. -
functions are basically free: make more of them. In my experience, if you’d use a phrase to name a piece of code when discussing it with a colleague, it should probably be a separate function. For instance, here’s the “read the file in” part from my day 8 solution:
def read(filename) do
File.stream!(filename)
|> Stream.map(&String.trim/1)
|> Stream.with_index()
|> Stream.flat_map(&parse_line/1)
|> Map.new()
end
defp parse_line({line, row_index}) do
line
|> String.codepoints()
|> Enum.map(&String.to_integer/1)
|> Enum.with_index()
|> Enum.map(fn {h, col_index} -> {{row_index, col_index}, h} end)
end
If you wanted %Tree{} structs like in your version, you’d change that very last statement of parse_line to build one out of row_index / col_index / h values.
Another guideline I find useful: repeat yourself, find the common parts, and then make THAT a function. For instance, you might notice this pattern (placeholders in SHOUTING_CASE):
trees_DIR = GET_TREES
visibility_score_DIR =
trees_DIR
|> visibility_score(current)
visible_DIR? =
trees_DIR
|> Enum.filter(fn x -> current <= x end)
|> Enum.empty?()
This becomes a function:
defp visibility_of(trees, current) do
score = visibility_score(trees, current)
flag =
trees
|> Enum.filter(fn x -> current <= x end)
|> Enum.empty? # NOTE: consider using any? instead of filter + empty?
{score, flag}
end
then the big branch of the case shortens to:
{row, column} ->
%{height: current, visible: _v} = Map.get(trees, {i, j})
{visibility_score_left, visible_left?} =
trees
|> traverse_x(column - 1, 0, row)
|> visibility_of(current)
{visibility_score_right, visible_right?} =
trees
|> traverse_x(column + 1, col_count, row)
|> visibility_of(current)
{visibility_score_up, visible_up?} =
trees
|> traverse_y(row - 1, 0, column)
|> visibility_of(current)
{visibility_score_down, visible_down?} =
trees
|> traverse_y(row + 1, row_count, column)
|> visibility_of(current)
%{
{i, j} => %Tree{
height: current,
visible: visible_up? || visible_down? || visible_left? || visible_right?,
score:
visibility_score_down * visibility_score_left * visibility_score_right *
visibility_score_up
}
}
Writing things this way makes it clearer that only the trees change between the four copies of the code.
kwando
Something I keep having use for in these problems where you have to walk around in a matrix is to use a list of “vectors” instead of hardcoding the movements.
This is for part 2, made a more “clever”/convoluted solution for part 1… but I had no use for in part 2.
defmodule VisibilityChecker do
def max_visibility(grid) do
heights = for {rows, y} <- Enum.with_index(grid),
{h, x} <- Enum.with_index(rows), into: %{} do
{{x, y}, h}
end
heights
|> Stream.map(&elem(&1, 0))
|> Stream.map(&score(&1, heights))
|> Enum.max()
end
@directions [{-1, 0},{1, 0},{0, -1},{0, 1},]
defp score(pos, heights) do
for dir <- @directions, reduce: 1 do
score -> score * visible(heights, pos, dir, heights[pos], 0)
end
end
defp visible(heights, pos, direction, max_height, line_height) do
new_pos = translate(pos, direction)
case heights[new_pos] do
nil -> 0
tree_height when tree_height >= max_height -> 1
tree_height when tree_height >= line_height ->
1 + visible(heights, new_pos, direction, max_height, tree_height)
_ ->
1 + visible(heights, new_pos, direction, max_height, line_height)
end
end
defp translate({x, y}, {dx, dy}), do: {x + dx, y + dy}
end
# input is a list of lists with the three heights [ [ 1, 2], [ 3, 4 ]]
#
# 1 2
# 3 4
#
# would be
# [
#. [ 1, 2 ],
# [ 3, 4 ]
# ]
VisibilityChecker.max_visibility(input)
Popular in Challenges
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








