👻 - 2024 DAY 19 SOLUTIONS -👻
Day 19 - Linen Layout
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Javascript
Behold an abomination!
MY EYES
Wanna try some Uiua?
Oh, my. That's... quite something.
Python
Approach: Recursive memoized backtracking with a Trie
I get to use one of my favorite data structures here, a Trie! It helps us figure out whether a prefix of the design is a valid pattern in linear time.
I use backtracking to choose potential component patterns (using the Trie), kicking off matching the rest of the design down the stack. We can continue matching longer patterns immediately after the recursion stack unwinds.
In addition, I use global memoization to keep track of the feasibility (part 1) or the number of combinations (part 2) for designs and sub-designs. This way, work done for earlier designs can help speed up later ones too.
I ended up combining part 1 and 2 solutions into a single function because part 1 is a simpler variant of part 2 where we count all designs with the number of possible pattern combinations > 0.
::: spoiler Reading Input
:::
::: spoiler Trie Implementation
:::
::: spoiler Solution
:::
Haskell
::: spoiler solution
:::
Haskell
My naive solution was taking ages until I tried matching from right to left instead :3
In the end the cache required for part two solved the problem more effectively.
A better version of
arrangementsusing laziness:My intuition nudged me there but I couldn't reason how that would change things. You still have to test the remaining string, and its remaining string, backtrack, etc right?
It was just a hunch that the inputs were generated to be difficult to parse using a naive algorithm (ie the towels have a lot of shared prefixes). In general I don't think there's any reason to suppose that one direction is better than the other, at least for random inputs.
Rust
I figured that Part 2 would want something to do with unique paths, so I tried to generate all paths in Part 1, which took too long. So I then decided to go with dynamic programming. In Part 1, I stored a cache of whether a given state can lead to the solution. In Part 2, I updated it to store how many options are possible from a given state.
https://gitlab.com/bricka/advent-of-code-2024-rust/-/blob/main/src/days/day19.rs?ref_type=heads
::: spoiler The Code
:::
Dart
Thanks to this useful post for reminding me that dynamic programming exists (and for linking to a source to help me remember how it works as it always makes my head spin :-) I guessed that part 2 would require counting solutions, so that helped too.
Solves live data in about 40ms.
Lovely! This is nicer than my memoized recursion. DP remains hard to wrap my head around even though I often apply it by accident.
Haskell
I had several strategy switches from brute-force to pathfinding (when doing part1 input instead of example) because It simply wouldn't finish. My solution only found the first path to the design, which is why I rewrote to only count how many towels there are for each prefix I have already built. Do that until there is either only one entry with the total combinations count or no entry and it's impossible to build the design.
I like the final solution, its small (unlike my other solutions) and runs fast.
::: spoiler 🚀
:::
C
Interestingly part 1 already defied a naive approach. It was fun thinking of a way to memoize without hash tables.
:::spoiler Code
:::
https://codeberg.org/sjmulder/aoc/src/branch/master/2024/c/day19.c
Zee
Also a port to my cursed Dutch dialect of C, Zee:
:::spoiler Code
:::
https://codeberg.org/sjmulder/aoc/src/
C#
Part 2 was pretty much the same as Part 2 except we can't short-circuit when we find the first match. So, implement a cache of each sub-pattern and the number of ways to form it from the towels, and things get much faster.
Python3
Solver uses trie just like the other python solve here, I try to not look at solve threads until after it is solved. yet, I somehow got a solve that only takes ~7 ms. previously I would rebuild the trie every time and made it take ~55 ms.
:::spoiler Code
:::
Rust
Pretty similar to the other rust answer. This definitely requires ::: spoiler spoiler memoization ::: of some form, but when done right, is very performant. 122ms for both.
I don't know much about Rust but I assume the
HashMap<String, i64>requires hashing on insertion and lookup, right? I realized that, for every design, all the strings you'll see are substrings of that design from different starting positions, so I made my lookup tableint pos -> int count. The table is reset after every design.That does mean that if two or more strings end with the same substring, you'd recalculate those substrings? Would be a faster lookup cost though, clever.
My code ran in 120ms, so its pretty damn fast as is, especially compared to the non-memoised version
edit: Tried the array of lengths method, shaved about 20ms off. Not bad, but probably not my main issue either
I hadn't really considered that, but yes. I'm inclined to think that replacing hash table lookups with plain array indexing (which this allows) outweighs that downside but I'm not sure. Indeed 120ms is pretty damn fast!
It saved me 20ms, and given your using C, saved you dealing with uthash or similar, so probably worth it.
The hashmap is probably a more generic solution though
Certainly more generic and less prone to user error too. Indeed dealing with hash maps or any advanced data structure is a pain with C, this is where generics or templates really shine, especially if you have control over lifetime aspects as you do with C++ or Rust (e.g. moves, lvalue references, constness, etc).
Haskell
Runs in 115 ms. Today's pretty straight forward. Memoization feels like magic sometimes!
::: spoiler Code
:::
Rust
First part is solved by making a regex of the available towels, like
^(r|wr|bg|bwu|rb|gb|br)*$for the example. If a design matches it, then it can be made. This didn't work for the second part, which is done using recursion and memoization instead. Again, it was quite surprising to see such a high solution number. 32 bits were not enough (thanks, debug mode overflow detection).::: spoiler Solution
:::
Also on github
How fast was the regex approach?
About 3ms. A manual implementation might be a bit faster, but not by much. The regex crate is quite optimized for pretty much these problems.
Wow, that is very fast, nice. I was happy with 120ms, seems I'm leaving a lot of performance on the table.
Edit: Regex cut my total time in half, but I am measuring the whole execution, still a massive improvement.
The 3ms are for part 1 only, part 2 takes around 27ms. But I see that our approaches there are very similar. One difference that might make an impact is that you copy the substrings for inserting into the hashmap into
Strings.Removing the string copy with the length->count array from @sjmulder saved me 20ms, so not super significant. I'll have to play the the profiler and see what I am doing wrong.
I think your approach looks a lot more Rust-like, which I like. Part 1 in 4 lines is very nice.
Rust
Definitely not Aho–Corasick cool or genius, but does get ~11ms on both parts. Gains over the other Rust solves are partly a less naïve approach to towel checking and partly getting it to work with 0
Stringallocations, which involves both an instructive lifetime annotation and the clever use of a niche standard library function. ::: spoiler Code:::
nice job! now do it without recursion. something I always attempt to shy away from as I think it is dirty to do, makes me feel like it is the "lazy man's" loop.
Aho-Corasick is definitely meant for this kind of challenge that requires finding all occurrences of multiple patterns, something worth reading up on! If you are someone who builds up a util library for future AoC or other projects then that would likely come in to use sometimes.
Another algorithm that is specific to finding one pattern is the Boyer-Moore algorithm. something to mull over: https://softwareengineering.stackexchange.com/a/183757
have to remember we are all discovering new interesting ways to solve similar or same challenges. I am still learning lots, hope to see you around here and next year!
C#
I had an error in the escape clause of the recursion that stumped me for a bit - wasn't counting the last towel!
This might be the first time I have ever had to use a long/ulong in 9 years of C# dev! (corp dev is obviously boring)
::: spoiler spoiler using System.Collections.Concurrent;
namespace AoC2024.day_19;
public class Day19 { private ConcurrentDictionary<string,ulong> _cachedPossibilities = new ConcurrentDictionary<string, ulong>();
} :::