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
Rust
Pretty similar to the other rust answer. This definitely requires
spoiler
memoization
of some form, but when done right, is very performant. 122ms for both.
#[cfg(test)] mod tests { use std::collections::HashMap; fn count_solutions( design: &str, patterns: &[&str], seen_designs: &mut HashMap<String, i64>, ) -> i64 { if design.is_empty() { return 1; } if let Some(s) = seen_designs.get(design) { return *s; } let mut count = 0; for pattern in patterns { if design.starts_with(pattern) { count += count_solutions(&design[pattern.len()..], patterns, seen_designs); } } seen_designs.insert(design.to_string(), count); count } #[test] fn day19_both_test() { let input = std::fs::read_to_string("src/input/day_19.txt").unwrap(); let parts = input.split_once("\n\n").unwrap(); let patterns = parts.0.split(", ").collect::<Vec<&str>>(); let designs = parts.1.split('\n').collect::<Vec<&str>>(); let mut count = 0; let mut total = 0; let mut seen_designs = HashMap::new(); for design in designs { let shortlist = patterns .iter() .filter_map(|p| { if design.contains(p) { return Some(*p); } None }) .collect::<Vec<&str>>(); let sol_count = count_solutions(design, &shortlist, &mut seen_designs); total += sol_count; count += (sol_count != 0) as usize; } println!("{}", count); println!("{}", total); } }
Javascript
Behold an abomination!
const input = require('fs').readFileSync(0, 'utf-8').toString(); const towels = new Set(input.split(/\r?\n\r?\n/g)[0].split(', ')); const count = (p, t) => [...new Array(p.length).keys()].reduce((acc, i) => [...new Array(i + 1).keys()].forEach(j => acc[j] > 0n && t.has(p.substring(j, i + 1)) ? acc[i + 1] += acc[j] : null) ? acc : acc, [1n, ...new Array(p.length).fill(0n)])[p.length]; input.split(/\r?\n\r?\n/g)[1].split(/\r?\n/g).filter(p => p.length > 0).reduce((acc, p) => { let c = count(p, towels); acc[0] += c > 0 ? 1 : 0; acc[1] += c; return acc }, [0, 0n]).forEach((v, i) => console.log(`Part ${i+1}: ${v}`));
Wanna try some Uiua?
MY EYES
Oh, my. That’s… quite something.
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
The Code
use std::collections::HashMap; use crate::solver::DaySolver; fn parse_input(input: String) -> (Vec<String>, Vec<String>) { let towels = input.lines().take(1).collect::<String>().split(", ").map(|s| s.to_string()).collect(); let designs = input.lines().skip(2).map(|s| s.to_string()).collect(); (towels, designs) } fn how_many_ways(cache: &mut HashMap<String, usize>, towels: &[String], current: String, target: &str) -> usize { if let Some(ways) = cache.get(¤t) { *ways } else if current == target { cache.insert(current.clone(), 1); 1 } else if !target.starts_with(¤t) { cache.insert(current.clone(), 0); 0 } else { let ways = towels.iter() .map(|t| format!("{}{}", current, t)) .map(|next| how_many_ways(cache, towels, next, target)) .sum(); cache.insert(current, ways); ways } } pub struct Day19Solver; impl DaySolver for Day19Solver { fn part1(&self, input: String) -> String { let (towels, designs) = parse_input(input); designs.into_iter() .filter(|d| how_many_ways(&mut HashMap::new(), &towels, "".to_string(), d) > 0) .count() .to_string() } fn part2(&self, input: String) -> String { let (towels, designs) = parse_input(input); designs.into_iter() .map(|d| how_many_ways(&mut HashMap::new(), &towels, "".to_string(), &d)) .sum::<usize>() .to_string() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_part1() { let input = include_str!("../../inputs/test/19"); let solver = Day19Solver {}; assert_eq!("6", solver.part1(input.to_string())); } #[test] fn test_part2() { let input = include_str!("../../inputs/test/19"); let solver = Day19Solver {}; assert_eq!("16", solver.part2(input.to_string())); } }
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.
using System.Collections.Immutable; using System.Diagnostics; using Common; namespace Day19; static class Program { static void Main() { var start = Stopwatch.GetTimestamp(); var sampleInput = ReceiveInput("sample.txt"); var programInput = ReceiveInput("input.txt"); Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}"); Console.WriteLine($"Part 1 input: {Part1(programInput)}"); Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}"); Console.WriteLine($"Part 2 input: {Part2(programInput)}"); Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}"); } static object Part1(Input input) { return input.Patterns .Select(p => AnyTowelMatches(p, input.Towels) ? 1 : 0) .Sum(); } static object Part2(Input input) { var matchCache = new Dictionary<string, long>(); return input.Patterns .Select(p => CountTowelMatches(p, input.Towels, matchCache)) .Sum(); } private static bool AnyTowelMatches( string pattern, ImmutableArray<string> towels) { return towels .Where(t => t.Length <= pattern.Length) .Select(t => !pattern.StartsWith(t) ? false : (pattern.Length == t.Length) ? true : AnyTowelMatches(pattern.Substring(t.Length), towels)) .Any(r => r); } private static long CountTowelMatches( string pattern, ImmutableArray<string> towels, Dictionary<string, long> matchCache) { if (matchCache.TryGetValue(pattern, out var count)) return count; count = towels .Where(t => t.Length <= pattern.Length) .Select(t => !pattern.StartsWith(t) ? 0 : (pattern.Length == t.Length) ? 1 : CountTowelMatches(pattern.Substring(t.Length), towels, matchCache)) .Sum(); matchCache[pattern] = count; return count; } static Input ReceiveInput(string file) { using var reader = new StreamReader(file); var towels = reader.ReadLine()!.SplitAndTrim(',').ToImmutableArray(); var patterns = new List<string>(); reader.ReadLine(); var line = reader.ReadLine(); while (line is not null) { patterns.Add(line); line = reader.ReadLine(); } return new Input() { Towels = towels, Patterns = [..patterns], }; } public class Input { public required ImmutableArray<string> Towels { get; init; } public required ImmutableArray<string> Patterns { get; init; } } }
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.
🚀
import Control.Arrow import Data.Map (Map) import qualified Data.List as List import qualified Data.Map as Map parse :: String -> ([String], [String]) parse = lines . init >>> (map (takeWhile (/= ',')) . words . head &&& drop 2) countDesignPaths :: [String] -> String -> Map Int Int -> Int countDesignPaths ts d es | Map.null es = 0 | ml == length d = mc | otherwise = countDesignPaths ts d es'' where ((ml, mc), es') = Map.deleteFindMin es ns = List.filter (flip List.isPrefixOf (List.drop ml d)) >>> List.map length >>> List.map (ml +) $ ts es'' = List.foldl (\ m l' -> Map.insertWith (+) l' mc m) es' $ ns solve (ts, ds) = List.map (flip (countDesignPaths ts) (Map.singleton 0 1)) >>> (List.length . List.filter (/= 0) &&& List.sum) $ ds main = getContents >>= print . solve . parse
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 80ms.
import 'package:collection/collection.dart'; import 'package:more/more.dart'; int countTarget(String target, Set<String> towels) { int n = target.length; List<int> ret = List.filled(n + 1, 0)..[0] = 1; for (int i in 1.to(n + 1)) { for (int j in 0.to(i).where((j) => ret[j] > 0)) { if (towels.contains(target.substring(j, i))) ret[i] += ret[j]; } } return ret[n]; } List<int> allCounts(List<String> lines) { var towels = lines.first.split(', ').toSet(); return lines.skip(2).map((p) => countTarget(p, towels)).toList(); } part1(List<String> lines) => allCounts(lines).where((e) => e > 0).length; part2(List<String> lines) => allCounts(lines).sum;
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.
Reading Input
import os here = os.path.dirname(os.path.abspath(__file__)) # read input def read_data(filename: str): global here filepath = os.path.join(here, filename) with open(filepath, mode="r", encoding="utf8") as f: return f.read()
Trie Implementation
class Trie: class TrieNode: def __init__(self) -> None: self.children = {} # connections to other TrieNode self.end = False # whether this node indicates an end of a pattern def __init__(self) -> None: self.root = Trie.TrieNode() def add(self, pattern: str): node = self.root # add the pattern to the trie, one character at a time for color in pattern: if color not in node.children: node.children[color] = Trie.TrieNode() node = node.children[color] # mark the node as the end of a pattern node.end = True
Solution
def soln(filename: str): data = read_data(filename) patterns, design_data = data.split("\n\n") # build the Trie trie = Trie() for pattern in patterns.split(", "): trie.add(pattern) designs = design_data.splitlines() # saves the design / sub-design -> number of component pattern combinations memo = {} def backtrack(design: str): nonlocal trie # if design is empty, we have successfully # matched the caller design / sub-design if design == "": return 1 # use memo if available if design in memo: return memo[design] # start matching a new pattern from here node = trie.root # number of pattern combinations for this design pattern_comb_count = 0 for i in range(len(design)): # if design[0 : i+1] is not a valid pattern, # we are done matching characters if design[i] not in node.children: break # move along the pattern node = node.children[design[i]] # we reached the end of a pattern if node.end: # get the pattern combinations count for the rest of the design / sub-design # all of them count for this design / sub-design pattern_comb_count += backtrack(design[i + 1 :]) # save the pattern combinations count for this design / sub-design memo[design] = pattern_comb_count return pattern_comb_count pattern_comb_counts = [] for design in designs: pattern_comb_counts.append(backtrack(design)) return pattern_comb_counts assert sum(1 for dc in soln("sample.txt") if dc > 0) == 6 print("Part 1:", sum(1 for dc in soln("input.txt") if dc > 0)) assert sum(soln("sample.txt")) == 16 print("Part 2:", sum(soln("input.txt")))
C#
public class Day19 : Solver { private string[] designs; private class Node { public Dictionary<char, Node> Children = []; public bool Terminal = false; } private Node root; public void Presolve(string input) { List<string> lines = [.. input.Trim().Split("\n")]; designs = lines[2..].ToArray(); root = new(); foreach (var pattern in lines[0].Split(", ")) { Node cur = root; foreach (char ch in pattern) { cur.Children.TryAdd(ch, new()); cur = cur.Children[ch]; } cur.Terminal = true; } } private long CountMatches(Node cur, Node root, string d) { if (d.Length == 0) return cur.Terminal ? 1 : 0; if (!cur.Children.TryGetValue(d[0], out var child)) return 0; return CountMatches(child, root, d[1..]) + (child.Terminal ? CountMatches(root, d[1..]) : 0); } private readonly Dictionary<string, long> cache = []; private long CountMatches(Node root, string d) { if (cache.TryGetValue(d, out var cached_match)) return cached_match; long match = CountMatches(root, root, d); cache[d] = match; return match; } public string SolveFirst() => designs.Where(d => CountMatches(root, d) > 0).Count().ToString(); public string SolveSecond() => designs.Select(d => CountMatches(root, d)).Sum().ToString(); }
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.
import Control.Arrow import Control.Monad.State import Data.List import Data.List.Split import Data.Map (Map) import Data.Map qualified as Map arrangements :: [String] -> String -> Int arrangements atoms = (`evalState` Map.empty) . go where go "" = return 1 go molecule = let computed = do c <- sum <$> mapM (\atom -> maybe (return 0) go $ stripPrefix atom molecule) atoms modify (Map.insert molecule c) return c in gets (Map.!? molecule) >>= maybe computed return main = do (atoms, molecules) <- (lines >>> (splitOn ", " . head &&& drop 2)) <$> readFile "input19" let result = map (arrangements atoms) molecules print . length $ filter (> 0) result print . sum $ result