Advent of Code 2022 - Day 6

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? :wave:

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.

2 Likes

Very short code, it’s basically 3 lines :slight_smile: 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
1 Like

Virtually identical to others

1 Like

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. :dancer: 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.

1 Like

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 :cry:

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")
1 Like