It is that time of the year again: Advent of Code 2022
Leaderboard:
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:
Isn’t this code losing the latest elf?
- |> elem(1)
+ |> then(fn {c, e} -> [c | e] end)
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.
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
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,
I used Stream.chunk_every
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
)
I used a very naive approach, I believe using chunk can turn out to be a better solution
String.split(input, "\n\n")
|> Stream.map(fn(x) ->
String.split(x)
|> Stream.map(&String.to_integer/1)
|> Enum.sum()
end)
|> Enum.max()
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()
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
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
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
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!
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:
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()
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
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