After the complexity of day 20, the solution to day 21 was confusingly simple. I repeatedly tried to solve it, got myself tied in knots, realised what I was doing was simpler than I thought, and tried again. The key, eventually, lay in the data structures.
A Food
comprises a set of Ingredient
s and a set of Allergen
s. The key data structure is the CandidateIngredients
, a map from an allergen to the ingredients that could contain it.
type Ingredient = String
type Allergen = String
data Food = Food
{ ingredients :: S.Set Ingredient
, allergens :: S.Set Allergen
} deriving (Show, Eq)
type CandidateIngredients = M.Map Allergen (S.Set Ingredient)
Parsing the input is simple enough.
foodsP = foodP `sepBy` endOfLine
foodP = Food <$> ingredientsP <* " (contains " <*> allergensP <* ")"
ingredientsP = S.fromList <$> (many1 letter) `sepBy` (many1 space)
allergensP = S.fromList <$> (many1 letter) `sepBy` (string ", ")
Finding candidate ingredients
The first step is to identify the candidate ingredients, the ingredients that could contain each allergen. From the example foods
- mxmxvkd kfcds sqjhc nhms (contains dairy, fish)
- trh fvjkl sbzzf mxmxvkd (contains dairy)
- sqjhc fvjkl (contains soy)
- sqjhc mxmxvkd sbzzf (contains fish)
I can create one CandidateIngredients
map for each food:
- dairy → {mxmxvkd, kfcds, sqjhc, nhms} ; fish → {mxmxvkd, kfcds,sqjhc, nhms}
- dairy → {trh, fvjkl, sbzzf, mxmxvkd}
- soy → {sqjhc, fvjkl}
- fish → {sqjhc, mxmxvkd, sbzzf}
If I combine the information about the first two foods, I don't gain any more information about which ingredients could contain fish. But I do now know that the ingredient that contains dairy must be in both sets of ingredients for dairy: it must be one of {mxmxvkd, kfcds, sqjhc, nhms} ∩ {trh, fvjkl, sbzzf, mxmxvkd} = {mxmxvkd}. If I combine all four of those CandidateIngredient
maps, finding hte intersections of ingredients for each allergen, I end up with a single map showing the possible ingredients for each allergen.
- dairy → {mxmxvkd} ; fish → {mxmxvkd, sqjhc} ; soy → {fvjkl, sqjhc}
Creating a CandidateIngredients
map for a single food is a one-liner, as is combining the maps into the final map.
candidates = M.unionsWith S.intersection $ map allergenMap foods
allergenMap :: Food -> CandidateIngredients
allergenMap food = M.fromList $ S.toList $ S.map (, ingredients food) $ allergens food
It took me a while to realise the solution to this problem was a simple as this!
Solving the tasks
Given the candiates
, both tasks become very simple.
For part 1, I remove the unsafe ingredients from each food, count how many are left, and sum the results.
part1 candidates foods = sum $ map countSafe foods
where possibleAllergens = S.unions $ M.elems candidates
countSafe food = S.size $ (ingredients food) `S.difference` possibleAllergens
Part 2 is an even simpler constraint satisfaction problem than day 16: allocate one ingredient to each allergen so that each allergen has a distinct ingredient. I just reused that CSP solution:
part2 candidates = intercalate "," $ map snd $ sortOn fst assignments
where assignments = knownAllergens candidates
knownAllergens :: CandidateIngredients -> [(Allergen, Ingredient)]
knownAllergens candidates = zip allergens assignedIngredients
where
(allergens, possibleIngredients) = unzip $ M.toList candidates
assignedIngredients = solveAllergens $ map S.toList possibleIngredients
solveAllergens :: [[Ingredient]] -> [Ingredient]
solveAllergens possibleIngredients = oneCSPSolution $ do
dvs <- mapM mkDV possibleIngredients
mapAllPairsM_ (constraint2 (/=)) dvs
return dvs
Code
You can find the code here or on GitLab.