lud

lud

Advent of Code 2024 - Day 19

This one was scary at first.

I loved how part one directs you into an obvious optimization, and then part 2 kicks your butt by asking to not do that very specific optimization :smiley:

My solution for part 2 runs in between 150ms and 380ms. I guess it’s because of the async streams, but it’s always fast (around 150, rarely even 120) when I run it once, but then with benchee for 10 seconds it averages between 170 and 380 depending on the runs …) Anyways, is fast enough.

defmodule AdventOfCode.Solutions.Y24.Day19 do
  alias AoC.Input

  def parse(input, _part) do
    [towels, targets] = input |> Input.read!() |> String.trim() |> String.split("\n\n")

    towels = towels |> String.split([",", " "], trim: true) |> Enum.map(&{&1, byte_size(&1)})
    targets = String.split(targets, "\n", trim: true)
    {towels, targets}
  end

  def part_one({towels, targets}) do
    primitives = reduce_towels(towels)
    targets = filter_possible_targets(targets, primitives)
    length(targets)
  end

  # remove towels that can be constructed with other towels
  defp reduce_towels(towels) do
    case Enum.split_with(towels, fn {text, _} = t -> possible_target?(text, towels -- [t]) end) do
      {[], all_primitives} -> all_primitives
      {_composable, primitives} -> reduce_towels(primitives)
    end
  end

  defp filter_possible_targets(targets, towels) do
    targets
    |> Task.async_stream(&{possible_target?(&1, towels), &1}, ordered: false, timeout: :infinity)
    |> Enum.flat_map(fn
      {:ok, {true, t}} -> [t]
      {:ok, {false, _}} -> []
    end)
  end

  defp possible_target?(target, towels) do
    possible_target?(target, towels, towels)
  end

  defp possible_target?("", _, _), do: true
  defp possible_target?(_, [], _), do: false

  defp possible_target?(target, [{h, b} | t], towels) do
    sub_match? =
      case target do
        <<^h::binary-size(b), rest::binary>> -> possible_target?(rest, towels, towels)
        _ -> false
      end

    sub_match? || possible_target?(target, t, towels)
  end

  def part_two({towels, targets}) do
    primitives = reduce_towels(towels)

    targets
    |> filter_possible_targets(primitives)
    |> Task.async_stream(&count_combinations(&1, towels), ordered: false, timeout: :infinity)
    |> Enum.reduce(0, fn {:ok, n}, acc -> acc + n end)
  end

  defp count_combinations(target, towels) do
    possible_towels = Enum.filter(towels, fn {text, _} -> String.contains?(target, text) end)
    do_count(%{target => 1}, possible_towels, 0)
  end

  defp do_count(target_suffixes, _towels, count) when map_size(target_suffixes) == 0 do
    count
  end

  defp do_count(target_suffixes, towels, count) do
    new_suffixes =
      for {t, count} <- target_suffixes, {h, b} <- towels, reduce: [] do
        sufxs ->
          case t do
            <<^h::binary-size(b), rest::binary>> -> [{rest, count} | sufxs]
            _ -> sufxs
          end
      end

    {target_suffixes, finished_count} =
      Enum.reduce(new_suffixes, {%{}, 0}, fn
        {"", cpt}, {map, finished_count} -> {map, finished_count + cpt}
        {sufx, cpt}, {map, finished_count} -> {Map.update(map, sufx, cpt, &(&1 + cpt)), finished_count}
      end)

    do_count(target_suffixes, towels, count + finished_count)
  end
end

Most Liked

bjorng

bjorng

Erlang Core Team

My straightforward solution for part 1 didn’t terminate for my real input.

I then implemented a trie (prefix tree). That took me a while, but it still didn’t terminate.

I then added memoization using the process dictionary. That worked.

After solving part 2, I cleaned up my code. I tried to use Memoize for memoization but the time increased to 31 seconds. I did some attempts to make Memoize use only the first argument of my count function, but I couldn’t make it work. In the end, I rewrote my count function to take an explicit memo argument.

The combined runtime for both parts and the examples is 0.4 seconds.

lkuty

lkuty

I compiled a big regex for part 1. It is slow and does not work for part 2 but it was trivial to implement. Now I have to find another kind of solution to be able to do part 2 and probably part 1 faster.

{towels, designs} = puzzle_input
|> String.split("\n")
|> then(fn [towels, _ | designs] -> {String.split(towels, ", "), designs} end)

{:ok, regex} = towels |> Enum.join("|") |> then(fn x -> "^(#{x})+$" end) |> Regex.compile()

designs
|> Enum.reduce(0, fn design, n -> if String.match?(design, regex), do: n+1, else: n end)
|> IO.inspect(label: "Part 1")

igorb

igorb

I started with a straightforward solution I coded up in a few minutes, but it was taking too long on the real input, so I spent a while to rewrite it with a prefix tree (trie) instead. It ended up being too slow too, at which point I realized that, of course, I just needed to use memoization. So then I added it and was able to get the final answer. Ironically, it turned out that my original solution just lacked memoization as well so after adding it it ended up being even faster. Though both are slow compared to your runtime—definitely takes a few seconds for me. I didn’t parallelize, though.

With prefix tree and custom memoization: advent-of-code-2024/lib/advent_of_code2024/day19_trie.ex at main · ibarakaiev/advent-of-code-2024 · GitHub

Straightforward, using a nice memoization library: advent-of-code-2024/lib/advent_of_code2024/day19.ex at main · ibarakaiev/advent-of-code-2024 · GitHub

sevenseacat

sevenseacat

Author of Ash Framework

Part 1 was nice and easy and I was amazed that the DFS I coded worked on literally the first try. That never ever happens.

Part 2 was a spanner in the works though - I have (what I think is) a nice solution, I’m not sure the name of the technique/algorithm I used though. Maybe memoization that everyone keeps mentioning.

I use a priority queue to keep track of the leftover strings to process for each requested towel, and a cache to track how many times I’ve seen each string.

If a string comes up in the queue and I’ve seen it before, then I can discard it and increment the number of different ways we’ve gotten to this point in the cache (by the number of ways we got to the previous point). Then, when the empty string comes up in the queue, I’ve seen all the ways to get here and can pluck it out of my cache. Works pretty well!

Name                     ips        average  deviation         median         99th %
day 19, part 1         29.38       34.03 ms     ±0.86%       34.01 ms       35.47 ms
day 19, part 2         29.36       34.06 ms     ±1.18%       34.03 ms       36.73 ms
OneEyed

OneEyed

Mine takes about 50ms per part, using in-process memoization since mixing designs does not seem to bring much.

defmodule AdventOfCode.Solution.Year2024.Day19 do
  use AdventOfCode.Solution.SharedParse

  @impl true
  def parse(input),
    do: String.split(input, "\n", trim: true) |> then(&{String.split(hd(&1), ", "), tl(&1)})

  def part1({patterns, designs}), do: run(patterns, designs) |> Enum.count(&(&1 > 0))

  def part2({patterns, designs}), do: run(patterns, designs) |> Enum.sum()

  defp run(patterns, designs) do
    Task.async_stream(designs, &ways(&1, patterns), ordered: false)
    |> Stream.map(fn {:ok, n} -> n end)
  end

  defp ways("", _), do: 1

  defp ways(design, patterns) do
    with nil <- Process.get(design) do
      Enum.reduce(patterns, 0, fn pattern, total ->
        case design do
          <<^pattern::binary, rest::binary>> -> ways(rest, patterns) + total
          _ -> total
        end
      end)
      |> tap(&Process.put(design, &1))
    end
  end
end

Where Next?

Popular in Challenges Top

code-shoily
Just did part 1. Part 2 seems to be demanding too much of my reading time so will get to that after I am done with some chores. Oh here ...
New
lud
Gosh this one took me sooo much time. At first I was trying to iterate each digit independently on the input A number to make digits cha...
New
Qqwy
Note by the Moderators: This topic is to talk about Day 6 of the Advent of Code. For general discussion about the Advent of Code 2018 an...
New
LostKobrakai
This one has been quite the ride. Struggled at first to find a good data format to suite the problem. I really like how that turned out b...
New
bjorng
My solution finishes both parts in 5 seconds on my computer. That time should be possible to reduce by optimizing my rather naive tilt/2 ...
New
Aetherus
I tried to use combinatorial to solve today’s puzzles but failed (my brain burned out :exploding_head:). In the end I just used brute for...
New
groovyda
Today’s challenge for me was about using reduce: defmodule Prob5 do def move([[h1 | rest] = _list1, list2]) do [rest, [h1 | list2]...
New
bjorng
This topic is about Day 9 of the Advent of Code 2021 . We have a private leaderboard (shared with users of Erlang Forums): https://adve...
New
New
coen.bakker
Since I started using Elixir, I have benefited greatly from being able to study various open-source projects. The codebase of LiveBook, i...
New

Other popular topics Top

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New

We're in Beta

About us Mission Statement