Part 1 completes in 61 microseconds using a guard. Not so good for part 2, where my feeble brain wasn’t capable of writing a guard for 14 characters and I went with Enum.uniq.
Generate them!
where are the reduce_while
folks?
defmodule ExAOC2022.Day6 do
@input "./lib/day6_input.txt"
def puzzle1() do
@input
|> File.read!()
|> distinct_chars_idx(4)
end
def puzzle2() do
@input
|> File.read!()
|> distinct_chars_idx(14)
end
defp distinct_chars_idx(str, len) do
str
|> String.graphemes()
|> Enum.with_index()
|> Enum.reduce_while([], fn {char, idx}, acc ->
if length(acc) < len do
{:cont, [char | acc]}
else
if has_dups?(acc) do
{:cont, [char | shorten(acc)]}
else
{:halt, idx}
end
end
end)
end
defp has_dups?(list), do: Enum.uniq(list) != list
defp shorten(list), do: list |> Enum.reverse() |> tl() |> Enum.reverse()
end
Could you give an example how to do it? I believe you`ll use macros but I dont know much about it would be nice if you could talk a little about it
The example is earlier up in the thread.
Very short code, it’s basically 3 lines because there isn’t much complexity. Forced myself to use pattern matching for fun.
defmodule Day6 do
# patternmatching fun!
# l = the list of chars, p = position where we are in the list
# s = the size of consecutive non-repetitive chars we're looking for
def find_marker(l, p, s ), do: find_marker(l, p, s, length(Enum.uniq(Enum.take(l,s))))
def find_marker(_, p, s, s), do: p+s
def find_marker(l, p, s, _), do: find_marker(tl(l), p+1, s)
end
list = File.read!("input") |> String.to_charlist()
Day6.find_marker(list, 0, 4) |> IO.inspect() # part 1
Day6.find_marker(list, 0, 14) |> IO.inspect() # part 2
Virtually identical to others
Today’s was weird. Part two didn’t require a single change aside from the window size. Just a single function call of:
@doc """
Finds the index of the next element after the first window of the given size
that contains unique elements
"""
@spec find_end_of_first_distinct_window([any()], pos_integer()) :: pos_integer()
def find_end_of_first_distinct_window(data_stream, window_size) do
data_stream
|> Enum.chunk_every(window_size, 1)
|> Enum.find_index(&Utilities.unique_elements?/1)
|> Kernel.+(window_size)
end
This pipeline is literally just the problem statement. The function Utilities.unique_elements?
just compares the length of an enum to the length of Enum.uniq
applied to the same enum. Full Livebook notebook here.
That’s only because elixir has very expressive means of working with windows on top of enumerables. If you consider imperative languages then var a; var b; var c; var d
is not as simply replaced with 14 manually juggled variables.
I should have reread the docs of chunk_every once more before deciding it wouldn’t work for this. I manually accumulated a list like a chump
Very very clean recursive solution! I like it!
Edit: FWIW, you can also do that:
Day6.find_marker(list, 0, 4) |> IO.inspect(label: "Part 1")
Day6.find_marker(list, 0, 14) |> IO.inspect(label: "Part 2")
Even in Elixir, for small lists directly comparing the items with each other is much faster than using a map to store seen elements (which is what Enum.uniq
does).
@bmitc’s function is 9x slower than the direct comparison of 4 items on my machine.
Comparison:
direct_compare 17.22 K
find_end_of_first_distinct_window 1.86 K - 9.28x slower +481.00 μs
However, the number of comparisons increases by n-1 for each element added, so there’s probably a point where using a map is faster.
Part 1
defmodule Part1 do
defguard is_start_of_packet_marker?(a, b, c, d) when a != b and a != c and a != d and b != c and b != d and c != d
def find_marker(signal) do
do_find_marker(signal, 0)
end
defp do_find_marker(<<a, b, c, d, _rest::binary>>, index) when is_start_of_packet_marker?(a, b, c, d) do
index + 4
end
defp do_find_marker(<<_a, b, c, d, rest::binary>>, index) do
do_find_marker(<<b, c, d, rest::binary>>, index + 1)
end
end
Part1.find_marker(input)
Part 2
defmodule Part2 do
def find_message(signal) do
do_find_message(signal, 0)
end
defp do_find_message(<<first, chars::binary-size(13), rest::binary>>, index) do
case is_message?(<<first, chars::binary-size(13)>>) do
true -> index + 14
false -> do_find_message(<<chars::binary-size(13), rest::binary>>, index + 1)
end
end
defp is_message?(chars) do
chars
|> String.codepoints()
|> Enum.uniq()
|> then(fn unique_chars -> length(unique_chars) == byte_size(chars) end)
end
end
Part2.find_message(input)
This puzzle was easy, I decided to go with Enum.reduce_while/3
.
Our company is switching to Elixir so I’m pretty new to the language and like to use the Advent puzzles as a learning tool.
My first solution was reasonably fast using a MapSet, but then I saw this video and came up with this. It runs in 200 µs here on part 2.
defmodule Day6 do
def read!(filename) do
File.read!(filename)
|> String.to_charlist()
end
def find_marker(data, count, index \\ 0) do
chunk =
Enum.slice(data, 0, count)
|> Enum.reverse()
case check_chunk(chunk, count, 0) do
:ok ->
index + count
{:error, fail_index} ->
find_marker(Enum.slice(data, fail_index..-1), count, index + fail_index)
end
end
defp check_chunk(_chunk, 0, _checker), do: :ok
defp check_chunk([char | chunk], count, checker) do
char = Bitwise.bsl(1, rem(char, 32))
case new_check = Bitwise.bor(checker, char) do
^checker -> {:error, count}
_ -> check_chunk(chunk, count - 1, new_check)
end
end
end
:timer.tc(fn ->
Day6.read!(filename)
|> Day6.find_marker(4)
end)
|> IO.inspect(label: "Part 1")
:timer.tc(fn ->
Day6.read!(filename)
|> Day6.find_marker(14)
end)
|> IO.inspect(label: "Part 2")