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!
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}`));
Haskell
solution
{-# LANGUAGE LambdaCase #-}
module Main where
import Control.Arrow
import Control.Monad.State
import Data.Char
import Data.List
import Data.Map qualified as M
import Data.Monoid
import Text.ParserCombinators.ReadP
parse = fst . last . readP_to_S ((,) <$> (patterns <* eol <* eol) <*> designs)
where
eol = char '\n'
patterns = sepBy word (string ", ")
designs = endBy word eol
word = munch1 isLetter
part1 patterns = length . filter (valid patterns)
part2 patterns = getSum . combinations patterns
dropPrefix = drop . length
valid :: [String] -> String -> Bool
valid patterns design = go design
where
go "" = True
go design = case filter (`isPrefixOf` design) patterns of
[] -> False
l -> any (go . (`dropPrefix` design)) l
combinations :: [String] -> [String] -> Sum Int
combinations patterns designs = evalState (fmap mconcat . mapM go $ designs) mempty
where
go "" = return $ Sum 1
go design =
gets (M.lookup design) >>= \case
Just c -> return c
Nothing -> case filter (`isPrefixOf` design) patterns of
[] -> return $ Sum 0
l -> do
res <- mconcat <$> mapM (go . (`dropPrefix` design)) l
modify (M.insert design res)
return res
main = getContents >>= print . (uncurry part1 &&& uncurry part2) . 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 40ms.
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 e in 1.to(n + 1)) {
for (int s in 0.to(e)) {
if (towels.contains(target.substring(s, e))) ret[e] += ret[s];
}
}
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")))
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