Got top 1000 for part 1, part 2 was super long so I took a while to understand it. Pretty fun day overall!
My solution:
import AOC
aoc 2023, 15 do
def p1(input) do
input |> String.split(",", trim: true) |> Enum.map(&hash/1) |> Enum.sum()
end
def hash(string) do
string
|> to_charlist()
|> Enum.reduce(0, fn ascii, hash -> rem((hash + ascii) * 17, 256) end)
end
def p2(input) do
input
|> String.split(",", trim: true)
|> Enum.reduce(%{},
fn command, boxes ->
if String.contains?(command, "=") do
[target, n] = String.split(command, "=")
box = hash(target)
n = String.to_integer(n)
stack = Map.get(boxes, box, [])
index = Enum.find_index(stack, fn {label, _} -> label == target end)
stack = if is_nil(index) do
[{target, n} | stack]
else
List.replace_at(stack, index, {target, n})
end
Map.put(boxes, box, stack)
else
target = String.trim(command, "-")
box = hash(target)
stack = Map.get(boxes, box, [])|> Enum.reject(fn {label, _} -> label == target end)
Map.put(boxes, box, stack)
end
end)
|> Enum.flat_map(fn {box, stack} -> stack |> Enum.reverse() |> Enum.with_index(1) |> Enum.map(fn {{_, focal}, slot} -> focal * slot * (box+1) end) end)
|> Enum.sum()
end
end
Not pretty, but it works
defmodule Day15 do
def hash_str(s) do
for ascii <- s |> String.to_charlist(),
reduce: 0 do
acc -> rem((acc + ascii) * 17, 256)
end
end
def put_lens_in_box(lens, []) do
[lens]
end
def put_lens_in_box([lens_id, length], lenses) do
contains_lens? = lenses |> Enum.any?(fn [lens, _] -> lens === lens_id end)
if contains_lens? do
lenses
|> Enum.map(fn [lens, len] ->
if lens == lens_id do
[lens, length]
else
[lens, len]
end
end)
else
lenses ++ [[lens_id, length]]
end
end
def handle_instruction({:add, [lens_id, length], dest}, state) do
lenses = Map.get(state, dest, [])
lenses = put_lens_in_box([lens_id, length], lenses)
state |> Map.put(dest, lenses)
end
def handle_instruction({:rem, [lens], dest}, state) do
lenses = Map.get(state, dest, [])
lenses = lenses |> Enum.filter(fn [bx_lens, _] -> bx_lens !== lens end)
state |> Map.put(dest, lenses)
end
def solve_1() do
{_, content} = File.read("./input/2023_15.txt")
for command <- content |> String.trim() |> String.split(","), reduce: 0 do
acc -> acc + hash_str(command)
end |> IO.puts()
end
def solve_2() do
{_, content} = File.read("./input/2023_15.txt")
for {instruction, arg} <-
[content]
|> Stream.flat_map(fn s ->
s
|> String.trim()
|> String.split(",")
end)
|> Stream.map(fn l ->
case String.contains?(l, "=") do
true ->
[lens, length] = String.split(l, "=")
{:add, [lens, length |> String.to_integer()]}
false ->
{:rem, [String.split(l, "-") |> Enum.at(0)]}
end
end),
reduce: %{} do
acc ->
dest = hash_str(arg |> Enum.at(0))
handle_instruction({instruction, arg, dest}, acc)
end
|> Enum.reduce(
0,
fn {box_id, lenses}, outer_acc ->
outer_acc +
for {[_, fcl_len], idx} <- lenses |> Enum.with_index(), reduce: 0 do
acc ->
acc + (box_id + 1) * (idx + 1) * fcl_len
end
end
)
|> IO.puts()
end
end
This one was a bit tedious but easy
My beginner’s solution, Day 15 part 1. Lens Library
defmodule Day15 do
def part1(input) do
parse(input)
|> Enum.map(&hash/1)
|> Enum.sum
end
def hash(string) do
for <<char::8 <- string>>, reduce: 0 do
acc -> rem(17 * (acc + char), 256)
end
end
def parse(raw_sequence) do
raw_sequence
|> remove("\n")
|> String.split(",")
end
defp remove(s, p), do: String.replace(s, p, "")
end
This was quite fun. Although I was lazy and used Aja.OrdMap
defmodule AdventOfCode.Y2023.Day15 do
alias AdventOfCode.Helpers.InputReader
alias Aja.OrdMap
def input, do: InputReader.read_from_file(2023, 15)
def run(input \\ input()) do
input = parse_1(input)
{run_1(input), run_2(input)}
end
defp run_1(input), do: Enum.reduce(input, 0, &(&2 + hash(&1)))
defp run_2(input) do
input
|> parse_2()
|> Enum.reduce(%{}, fn
{op, b, len}, acc ->
hash = hash(b)
case op do
:add -> Map.update(acc, hash, OrdMap.new(%{b => len}), &OrdMap.put(&1, b, len))
:remove -> Map.update(acc, hash, OrdMap.new(%{}), &OrdMap.drop(&1, [b]))
end
end)
|> total_focus()
end
defp total_focus(map) do
Enum.reduce(map, 0, fn {box, boxes}, acc ->
acc +
(boxes
|> OrdMap.to_list()
|> Enum.with_index(1)
|> Enum.reduce(0, &(&2 + row_focus(&1, box))))
end)
end
defp row_focus({{_, len}, slot}, box), do: (box + 1) * slot * len
def parse_1(input \\ input()), do: String.split(input, ",", trim: true)
def parse_2(commands) do
Enum.map(commands, fn cmd ->
case Regex.run(~r{([a-zA-Z]+)(=|-)([0-9]*)}, cmd) do
[_, label, "=", len] -> {:add, label, String.to_integer(len)}
[_, label, "-", ""] -> {:remove, label, 0}
end
end)
end
def hash(lst) do
for ch <- String.to_charlist(lst), reduce: 0 do
acc -> rem((acc + ch) * 17, 256)
end
end
end
This one seemed rather straightforward to me, no fancy algorithms or optimisations. My solution for part 2 does feel a bit clumsy in terms of list handling, but it works.
defmodule Main do
def hash(s), do: s |> String.to_charlist() |> Enum.reduce(0, fn c, h -> rem(17*(c+h),256) end)
def solve1(ls), do: ls |> Enum.map(&hash/1) |> Enum.sum()
def parse(i) do
[l,a,n] = Regex.run(~r/^([a-z]+)([-=])(\d*)$/,i) |> tl()
{l,hash(l),a,if n == "" do 0 else String.to_integer(n) end}
end
def solve2(ls) do
ls |> Enum.map(&parse/1)
|> Enum.reduce(%{}, fn {l,h,a,n}, d ->
case a do
"-" -> Map.update(d,h,[],&List.keydelete(&1,l,0))
"=" -> Map.update(d,h,[{l,n}], &( if List.keymember?(&1,l,0) do
List.keyreplace(&1,l,0,{l,n})
else &1 ++ [{l,n}] end))
end
end)
|> Enum.into([])
|> Enum.map(fn {h,bxl} ->
bxl |> Enum.with_index(1) |> Enum.map(fn {{_,p},i} -> (h+1)*i*p end) end)
|> Enum.concat() |> Enum.sum()
end
def run() do
get_input()
# |> solve1()
|> solve2()
end
def get_input() do
# "testinput15"
"input15"
|> File.read!() |> String.trim() |> String.split(",")
end
end
:timer.tc(&Main.run/0)
|> IO.inspect(charlists: :as_lists)
This day was easy.
I used regex too, especially named captures:
%{"label" => label, "op" => op, "length" => length} =
Regex.named_captures(~r/(?<label>[a-z]*)(?<op>(=|-))(?<length>[0-9]*)/, str)
imo it’s prettier than using String.contains? and String.split
case String.contains?(str, "=") do
true ->
... = String.split(str, "=")
...
false ->
... = String.split(str, "-")
...
end
Question: I tend to always use Enum.reduce/3 as I find it convenient to introduce it into a pipeline but I see a lot of people using:
for x <- enum, reduce: 0
acc -> ...
end
Can you tell me why do you prefer this syntax? No judgement, I’m just curious…
The problem was really straightforward on both parts for this day. Makes me sad that I got so far behind on days 12-14. I didn’t do anything clever in my solution. This thread showed me some things I had not heard of. Namely, @code-shoily introducing Aja and @exists highlighting List.keydelete
and List.keymember?
. I wanted the theme this year to be binary pattern matching and manipulation as much as possible, but I didn’t want to overcomplicate this one and just used the String
functions mostly.
defmodule Part1 do
def solve(input) do
input
|> parse()
|> Enum.sum()
end
defp parse(input) do
hasher(input, 0, [])
end
def hasher(<<>>, current_val, acc), do: [current_val | acc]
def hasher(<<",", rest::binary>>, current_val, acc), do: hasher(rest, 0, [current_val | acc])
def hasher(<<"\n", rest::binary>>, current_val, acc), do: hasher(rest, current_val, acc)
def hasher(<<next::8, rest::binary>>, current_val, acc),
do: hasher(rest, hash_fun(next, current_val), acc)
def hash_fun(next, current_val), do: rem((next + current_val) * 17, 256)
end
defmodule Part2 do
def solve(input) do
input
|> parse()
|> Enum.map(&calc_focus_pwr/1)
|> total_focus_pwr()
end
defp parse(input) do
input
|> String.replace("\n", "")
|> String.split(",")
|> Enum.map(&parse_label/1)
|> Enum.reduce(%{}, fn ops_label, map ->
hash_mapper(ops_label, map)
end)
end
defp parse_label(label) do
String.split(label, ~r{-|=}, include_captures: true)
end
defp hash_mapper([label, op, len], map) do
[box] = Part1.hasher(label, 0, [])
case op do
"-" ->
Map.update(map, box, [], fn curr ->
Enum.reject(curr, fn lens -> match?([^label, _], lens) end)
end)
"=" ->
Map.update(map, box, [[label, len]], fn curr ->
with nil <- Enum.find_index(curr, fn [a, _b] -> a == label end) do
[[label, len] | curr]
else
ndx ->
List.replace_at(curr, ndx, [label, len])
end
end)
end
end
defp calc_focus_pwr({box, lenses}) do
lenses
|> Enum.reverse()
|> Enum.with_index(1)
|> Enum.map(fn {[_label, lens], ndx} -> String.to_integer(lens) * ndx * (box + 1) end)
|> Enum.sum()
end
defp total_focus_pwr(enum), do: Enum.sum(enum)
end
Like you I mostly use Enum.reduce
. When I do reach for the for
comprehension syntax it’s usually to take advantage of how easy it is to combine multiple enumerables into one logic pipeline.
a = [1, 2, 3]
b = [?a, ?b, ?c]
for i <- a, c <- b, i + c == 100, reduce: 0 do
acc -> acc + i + c
end
# 300
versus
a
|> Enum.reduce(0, fn i, acc ->
b
|> Enum.filter(fn c -> c + i == 100 end)
|> Enum.reduce(acc, fn c, accacc ->
accacc + c + i end)
end)
# 300
I suspect it also may seem more familiar to people more comfortable with imperative programming approaches. Finally, there might be some cases where the for
comprehension does provide some performance improvements as per this discussion.
Thanks Steven for you reply
Syntax familiarity for people coming from the imperative world was the only one thing that came up to my mind.
Thanks for the link to the discussion. I haven’t read it before. I didn’t know for was more efficient than Enum. I’ll try to use it more often then. Advent of code is really nice, I always learn things when looking at the way others solved the same problem.