Note by the Moderators: This topic is to talk about Day 5 of the Advent of Code.
For general discussion about the Advent of Code 2018 and links to topics of the other days, see this topic.
This is my Day 5. My other days are a bit of a mess (constantly refactoring to try out different ways to make it look better). With day 5 though, I saw a lot of places for a bit of optimization like storing the length in the fully_react reduction.
My take-away from day 5’s code is to keep things simple. And read all of the documentation because I’ve been creating functions when I don’t need to.
My first attempt at the challenge was to do a single pass through the characters in pairs, remove them if the fitted the matching criteria which created a new string to parse and the changing the current ‘cursor’ position to handle the string length change so that I didn’t skip letters. It worked but it was as messy as it sounds (possibly more-so!) and it was really slow.
Solving part 2 whilst part one is slow is possible but I’d have probably had time for a nap whilst it ran so I decided to try a simpler approach. This time I converted the polymers into a character list (I originally used graphemes but saw one of a few great tricks in @sionide21’s solution… see below) so that I could use recursive pattern matched functions to process the polymers. If at the end of a run through the characters the resulting string was shorter than the original I’d re-process the characters and carry on until changes had been exhausted. This was much, much faster.
Part 2 was where I encountered some new standard library functions and in particular Enum.uniq/1 and Enum.reject/2, both of which allowed me to remove Enum.reduce/3 calls. The combination of Map.values/1 and Enum.min/1 were also really helpful at the end of the pipeline and saved me from writing yet another Enum.reduce/3.
After writing my code I looked at @sionide21’s solution which helped me tidy a few things up. He’d used recursive pattern matching functions too but passed a boolean flag between them to indicate whether the polymers had been edited. This is cleaner. He also took advantage of character codes to handle upper and lower case checks: abs(a - b) == 32. I’d used an if statement inside a single function but I thought of @pragdave and changed it and could then use two functions. If I’d retro-fitted the change flag in place of my string length parameter I could have done the same for the last function.
Overall I was really pleased with how today’s solution turned out and it was gratifying to see that I was heading in the same direction as more experienced Elixir developers.
My original solution was a pretty hacky regexy thing in rust. After I’d finished and looked at the solutions mega thread the Haskell foldr five liner at the top was a bit of a blow . I made an elixir version which uses lists rather than binaries which I think is nice too:
(EDIT: very similar to Ryan’s solution above too)
defmodule Day5 do
@moduledoc """
Day 5 Elixir solutions for Advent of Code 2018
"""
@doc """
Removes all reacting units from the input and returns the final length
Algorithm from a Haskell implementation in reddit:
https://www.reddit.com/r/adventofcode/comments/a3912m/2018_day_5_solutions/eb4dchg/
## Examples
iex> Day5.react("dabAcCaCBAcCcaDA")
10
"""
def react(input) do
input
|> String.to_charlist()
|> List.foldr([], &react/2)
|> Enum.count()
end
# two units react when the difference in their ASCII code is 32
defguard reacts(unit, prev_unit) when abs(unit - prev_unit) == 32
# handle the start case
defp react(unit, []), do: [unit]
# handle when the last two elements in the list react
defp react(unit, [prev_unit]) when reacts(unit, prev_unit), do: []
# if there's a reaction just return the tail to remove reacting elements
defp react(unit, [prev_unit | tail]) when reacts(unit, prev_unit), do: tail
# all good, just add the unit to the list
defp react(unit, tail), do: [unit | tail]
end
I wasn’t going to but it was irresistible… (EDIT: just realised you asked Gazler that not me!)
I rearranged things a bit to make it possible to pre-react the list before looking for the worst character to reduce the size of the list looked at in each iteration.
I’m using Advent of Code as an excuse to learn me some Elixir for great good.
In one of José Valim’s videos he mentioned how it’s always better to append at the head of a list than to append at the bottom of it, and oh wow, does it make a difference!
I had code like this in my functions: reduced = reduced_polymer ++ [head1] all because I wanted to preserve the order in the polymer.
So, my Day5 Part1 was initially clocking in at # Time in seconds: 162.826491.
Then I realized that it didn’t matter because I was not after the final string, only its length, and I could always reverse it at the end and get that if needed. So I changed that code to: reduced = [head1 | reduced_polymer] and it went down to: # Time in seconds: 0.28561000000000003 O_O.
Lesson learned!
I’ve been astonished at the speed of some of José’s solutions. I don’t think any of mine are ridiculously slow but comparatively they are.
Solving the problems is one element of the challenge. Making the code faster is another. Making it faster and still understandable is where I struggle the most as an Elixir newbie.
My plan is to re-do the challenges in January to see how my code has evolved over the challenge which is not a long period of time but I’m writing a lot of code and learning new things every day.