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

Aetherus
This topic is about Day 3 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
bismark
Took me a minute to remember my binary math :smile: :grimacing:.. import Bitwise __DIR__ |&gt; Path.join("puzzle.txt") |&gt; File.strea...
New
jkwchui
Monkeys fitted squarely as GenServers in my head. My initial problem was using cast instead of call; I imagine impolite monkeys slinging...
New
kwando
Took a while, but another use case for “move vectors” today and pattern matching. :slight_smile: The trick was to first generate a list...
New
mattbaker
I’m having so much fun working on the “Protohackers” challenges, I never got into Advent of Code much but this has been amazing. The chal...
New
bjorng
Note: This topic is to talk about Day 25 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
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
rvnash
Anyone have a solution to Part 2 today? Part 1 was straight forward, but I can’t figure out a programatic way to do part 2. I understand ...
New
bjorng
Note: This topic is to talk about Day 6 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
igorb
Today is a brute-force day: advent-of-code-2024/lib/advent_of_code2024/day6.ex at main · ibarakaiev/advent-of-code-2024 · GitHub Takes a...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
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
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement