Advent of Code 2022 - Day 1

It is that time of the year again: Advent of Code 2022 :christmas_tree:

Day 1

Leaderboard:

6 Likes

Here is my first attempt, which uses specific reduces for the task at hand: advent-of-code/day01.ex at b4357a22f3a53bb46e4a98852eef4049ab1b1ec6 · woolfred/advent-of-code · GitHub

Afterwards I refactored it a bit, which makes it less efficient for Part 1, but favours readability:

5 Likes

Isn’t this code losing the latest elf?

- |> elem(1)
+ |> then(fn {c, e} -> [c | e] end)
2 Likes

Good catch! This would be the case if the last elf wouldn’t end with an empty line like the others.
But luckily this is the case and therefore the elf is also added to the elves list.

Edit:
From the specs:

Each Elf separates their own inventory from the previous Elf’s inventory (if any) by a blank line.

So @mudasobwa you are right and if the input generator would be working like this, there shouldn’t be a empty line after the last elf.

1 Like

There is a leaderboard from last year that we can reuse. (shared with users of Erlang Forums ) /cc @bjorng

https://adventofcode.com/2022/leaderboard/private/view/370884

The entry code is:
370884-a6a71927

5 Likes

Here is my attempt

calories = File.stream!("input01") |>
Stream.map(&String.trim/1) |>
Stream.chunk_by(fn(x) -> x != "" end) |>
Stream.reject(fn(x) -> x == [""] end) |>
Enum.map(&( Enum.reduce(&1, 0, fn s, acc -> acc + String.to_integer(s) end)))

most = calories |> Enum.max
top3 = calories |> Enum.sort(:desc) |> Enum.take(3) |> Enum.sum()

It took me longer than expected, because I though Stream or Enum already have a function that split a list by token, so I spend a long time looking for it. In the end I settled on chunk_by + reject,

1 Like

I used Stream.chunk_every :slight_smile:

input = File.stream!("/Users/kwando/projects/AoC2022/01/input.txt")
|> Stream.map(&String.trim/1)
|> Stream.chunk_while(
  0,
  fn 
    "", chunk ->
      {:cont, chunk, 0}
    value, chunk ->
      {:cont, String.to_integer(value) + chunk}
  end,
  fn chunk -> {:cont, chunk, 0} end
)
2 Likes

I used a very naive approach, I believe using chunk can turn out to be a better solution

Part 1

String.split(input, "\n\n")
|> Stream.map(fn(x) ->
  String.split(x)
  |> Stream.map(&String.to_integer/1)
  |> Enum.sum()
end)
|> Enum.max()

Part 2

String.split(input, "\n\n")
|> Stream.map(fn(x) ->
  String.split(x)
  |> Stream.map(&String.to_integer/1)
  |> Enum.sum()
end)
|> Enum.sort(:desc)
|> Stream.take(3)
|> Enum.sum()
5 Likes

Mine is similar to @pesnk. Code and blog

defmodule Day01 do
  use AOC

  def part1 do
    input(1)
    ~> String.split("\n\n")
    ~> Enum.map(fn elf ->
      elf
      ~> String.split("\n")
      ~> Enum.reduce(0, fn a, b -> String.to_integer(a) + b end)
    end)
    ~> Enum.max()
  end

  def part2 do
    input(1)
    ~> String.split("\n\n")
    ~> Enum.map(fn elf ->
      elf
      ~> String.split("\n")
      ~> Enum.reduce(0, fn a, b -> String.to_integer(a) + b end)
    end)
    ~> Enum.sort(:desc)
    ~> Enum.slice(0, 3)
    ~> Enum.reduce(0, fn a, b -> a + b end)
  end

end
1 Like

Quickly in IEX

# elves = """
# ...
#"""

# Part 1
elves
|> String.split("\n\n")
|> Enum.map(& String.split(&1, "\n"))
|> Enum.map(fn calories ->
  calories
  |> Enum.map(& String.trim/1)
  |> Enum.reject(& &1 == "")
  |> Enum.map(& String.to_integer/1)
  |> Enum.sum
end)
|> Enum.with_index(1)
|> Enum.max(& >=/2, fn {calories, index} -> calories end)

# Part 2
elves
|> String.split("\n\n")
|> Enum.map(& String.split(&1, "\n"))
|> Enum.map(fn calories->
  calories
  |> Enum.map(& String.trim/1)
  |> Enum.reject(& &1 == "")
  |> Enum.map(& String.to_integer/1)
  |> Enum.sum
end)
|> Enum.with_index(1)
|> Enum.sort_by(fn {calories, _} -> calories end, :desc)
|> Enum.take(3)
|> Enum.map(fn {calories, _} -> calories end)
|> Enum.sum
1 Like

Pretty quick one here:

  def part_one do
    @input
    |> read_file()
    # reduce instead of doing Enum.max at the end should be potentially faster
    |> Enum.reduce(0, fn l, acc ->
      sum = Enum.sum(l)
      if sum > acc, do: sum, else: acc
    end)
  end

  def part_two do
    [l, m, s | _] =
      @input
      |> read_file()
      |> Enum.map(&Enum.sum(&1))
      |> Enum.sort(:desc)

    l + m + s
  end

  defp read_file(file) do
    file
    |> File.read!()
    |> String.split("\n\n")
    |> Enum.map(fn s ->
      s |> String.split("\n", trim: true) |> Enum.map(&String.to_integer/1)
    end)
  end
1 Like

I also took the most direct approach, although I did “refactor” slightly in part to to use reduce to avoid excessive iterations over the list. I wish I had thought of File.stream!

2 Likes

My lazy solution

lazy one

Shortest and the fastest solution I wrote in 15 minutes

defmodule AOC1 do
  def traverse(input, number \\ 0, current \\ 0, maximum \\ 0) do
    case input do
      "" ->
        max(maximum, number + current)

      "\n\n" <> tail ->
        traverse(tail, 0, 0, max(maximum, number + current))

      "\n" <> tail ->
        traverse(tail, 0, current + number, maximum)

      <<char, tail :: binary>> ->
        number = number * 10 + char - ?0
        traverse(tail, number, current, maximum)
    end
  end
end

IO.inspect AOC1.traverse IO.read(:eof)

Part 2

defmodule AOC12 do
  def traverse(input, number \\ 0, current \\ 0, maximum \\ {0, 0, 0}) do
    case input do
      "" ->
        {first, second, third} = put_max(number + current, maximum)
        first + second + third

      "\n\n" <> tail ->
        traverse(tail, 0, 0, put_max(number + current, maximum))

      "\n" <> tail ->
        traverse(tail, 0, current + number, maximum)

      <<char, tail :: binary>> ->
        number = number * 10 + char - ?0
        traverse(tail, number, current, maximum)
    end
  end

  defp put_max(new, {_first, second, third}) when new > third, do: {second, third, new}
  defp put_max(new, {_first, second, third}) when new > second, do: {second, new, third}
  defp put_max(new, {first, second, third}) when new > first, do: {new, second, third}
  defp put_max(_, maximum), do: maximum
end

IO.inspect AOC1.traverse IO.read :eof

This is pretty identical to my part 1 and part 2 solutions! Seems like it’s the easiest iterate-function-call-by-function-call pipeline approach to develop, though not parallelized.

TL;DR:

Part 1:
input_file
|> File.read!()
|> String.split("\n\n")
|> Enum.map(&String.split(&1, "\n"))
|> Enum.map(fn inputs ->
 inputs
 |> Enum.map(&String.to_integer/1)
 |> Enum.sum
end)
|> Enum.max()
Part 2:
input_file
|> File.read!()
|> String.split("\n\n")
|> Enum.map(&String.split(&1, "\n"))
|> Enum.map(fn inputs ->
  inputs
  |> Enum.map(&String.to_integer/1)
  |> Enum.sum
end)
|> Enum.sort(:desc)
|> Enum.take(3)
|> Enum.sum()

Like others, I used Stream.chunk_while/4

Outside of a hardcoded N with pattern matching tuples… whats the fastest way to get the top N elements in a list?

Benchee puts this at much faster than Enum.sort(:desc) |> Enum.take(n) (it assumes there are >N elements in the list…):

fn n, list ->
  topN = for _ <- 1..n, do: 0
  Enum.reduce(list, topN, fn
    el, [min | rest] when el > min -> Enum.sort([el|rest])
    _, topN -> topN
  end)
end
1 Like

If you just want to get the top 3 (not top N with N as a variable), then just

defp get_top3([], top1, top2, top3), do: [top1, top2, top3]

defp get_top3([h|t], top1, top2, _top3) when h > top1, do: get_top3(t, h, top1, top2)

defp get_top3([h|t], top1, top2, _top3) when h > top2, do: get_top3(t, top1, h, top2)

defp get_top3([h|t], top1, top2, top3) when h > top3, do: get_top3(t, top1, top2, h)

defp get_top3([_h|t], top1, top2, top3), do: get_top3(t, top1, top2, top3)

The fastest way is to traverse list and perform binary search in tuple of maximums (if it’s size more than 5), or linear search (for small tuples of maximums). Something like

# All indeces are 1-based, because it is more efficient in BEAM
def put_max(maxs, new) do
  if new <= :erlang.element(1, maxs) do
    maxs
  else
    put_max(maxs, new, 1, tuple_size(maxs))
  end
end

def put_max(maxs, new, index, index) do
  put_truncating(maxs, index, new)  
end

def put_max(maxs, new, left, right) do
  middle = div(left + right, 2)
  case :erlang.element(middle, maxs) do
    ^new ->
      put_truncating(tuple, middle, new)

    less when less < new ->
      put_max(maxs, new, left, middle)
 
    _ ->
      put_max(maxs, new, middle, right)
  end
end

defp put_truncating(tuple, index, value) do
  tuple = :erlang.insert_element(index, tuple,value)
  :erlang.delete_element(1, tuple)
end
1 Like