LostKobrakai

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

al2o3cr

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 Enum functions have a variant that lets you transform the input before doing their thing. For instance, Enum.count/2 or Enum.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.reduce with an initial value of %{} will be clearer with Map.new. I say “most” because sometimes there’s code in the block passed to reduce that returns acc unchanged, which you can’t do with Map.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 using Stream means 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

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)

Where Next?

Popular in Challenges Top

woolfred
It is that time of the year again: Advent of Code 2022 :christmas_tree: Day 1 Leaderboard:
New
QuinnWilton
Note: This topic is to talk about Day 7 of the Advent of Code 2019 . There is a private leaderboard for elixirforum members. You can joi...
New
bjorng
This topic is about Day 15 of the Advent of Code 2021. We have a private leaderboard (shared with users of Erlang Forums): https://adve...
New
bjorng
Here is my solution for day 2 of Advent of Code: https://github.com/bjorng/advent-of-code/blob/main/2024/day02/lib/day02.ex
New
New
bjorng
My solution finishes both parts in 5 seconds on my computer. That time should be possible to reduce by optimizing my rather naive tilt/2 ...
New
rugyoga
part 1: https://github.com/rugyoga/aoc2021/blob/main/day8.exs part 2: https://github.com/rugyoga/aoc2021/blob/main/day8b.exs
New
Aetherus
I spent 3 hours struggling in part 2, until I noticed a very basic mistake :joy: Here’s my code: https://github.com/Aetherus/advent-of-...
New
connorlay
Note by the Moderators: This topic is for general discussion about the Advent of Code 2018. To prevent people from being spoiled about s...
New
rugyoga
Fairly straightforward Dijkstra’s algorithm import AOC aoc 2023, 17 do def compute(input, candidates) do {{max_row, max_col}, ite...
New

Other popular topics Top

Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement