aoc/year2020/day21.rs
1//! # Allergen Assessment
2//!
3//! The rules can be expressed as:
4//!
5//! * If an ingredient is on a line, then it *may* contain the listed allergens.
6//! * If an ingredient is *not* on a line, then it definitely *does not* contain the listed
7//! allergens, as some other food on the line must instead contain the allergen.
8//!
9//! ## Part One
10//! To find the safe foods we build two sets, then subtract them to find out the remaining possible
11//! allergens. It's important to only subtract the sets at the very end in order to prevent
12//! re-adding a previously excluded allergen. Using `kfcds` from the example:
13//!
14//! | Line | Possible | Impossible |
15//! | --- | ----------- | ---------------- |
16//! | 1 | Dairy, Fish | Ø |
17//! | 2 | Dairy, Fish | Dairy |
18//! | 3 | Dairy, Fish | Dairy, Soy |
19//! | 4 | Dairy, Fish | Dairy, Soy, Fish |
20//!
21//! Final result: Ø (the empty set)
22//!
23//! # Part Two
24//! This is a [constraint satisfaction problem](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem),
25//! similar to [`day 16`]. Using `fvjkl` from the example:
26//!
27//! | Line | Possible | Impossible |
28//! | --- | ---------- | ----------- |
29//! | 1 | Ø | Dairy, Fish |
30//! | 2 | Dairy | Dairy, Fish |
31//! | 3 | Dairy, Soy | Dairy, Fish |
32//! | 4 | Dairy, Soy | Dairy, Fish |
33//!
34//! Final result: Soy
35//!
36//! To solve there must be at least one ingredient with only one allergen remaining.
37//! As this allergen can only belong to this ingredient, we eliminate it from other ingredients.
38//! This causes a chain reaction where a second ingredient will reduce to only one allergen,
39//! continuing until all allergens have been resolved.
40//!
41//! As there are less than 64 lines and allergens we can speed things up by using bitwise logic
42//! on a `usize` to compute set addition and subtraction. To add to a set use OR `|`,
43//! to remove use AND `&` and to calculate the size use [`count_ones`].
44//!
45//! [`Day 16`]: crate::year2020::day16
46//! [`count_ones`]: u32::count_ones
47use crate::util::hash::*;
48use std::collections::BTreeMap;
49
50pub struct Input<'a> {
51 ingredients: FastMap<&'a str, Ingredient>,
52 allergens: FastMap<&'a str, usize>,
53}
54
55#[derive(Clone, Copy, Default)]
56pub struct Ingredient {
57 food: usize,
58 candidates: usize,
59}
60
61pub fn parse(input: &str) -> Input<'_> {
62 let mut ingredients: FastMap<&str, Ingredient> = FastMap::new();
63 let mut allergens = FastMap::new();
64 let mut allergens_per_food = Vec::new();
65
66 for (i, line) in input.lines().enumerate() {
67 let (prefix, suffix) = line.rsplit_once(" (contains ").unwrap();
68
69 for ingredient in prefix.split_ascii_whitespace() {
70 let entry = ingredients.entry(ingredient).or_default();
71 entry.food |= 1 << i;
72 }
73
74 let mut mask = 0;
75 for allergen in suffix.split([' ', ',', ')']).filter(|a| !a.is_empty()) {
76 let size = allergens.len();
77 let entry = allergens.entry(allergen).or_insert(size);
78 mask |= 1 << *entry;
79 }
80 allergens_per_food.push(mask);
81 }
82
83 for ingredient in ingredients.values_mut() {
84 let mut possible = 0;
85 let mut impossible = 0;
86
87 for (i, allergens) in allergens_per_food.iter().enumerate() {
88 if ingredient.food & (1 << i) == 0 {
89 impossible |= allergens;
90 } else {
91 possible |= allergens;
92 }
93 }
94
95 ingredient.candidates = possible & !impossible;
96 }
97
98 Input { ingredients, allergens }
99}
100
101pub fn part1(input: &Input<'_>) -> u32 {
102 input.ingredients.values().filter(|i| i.candidates == 0).map(|i| i.food.count_ones()).sum()
103}
104
105pub fn part2(input: &Input<'_>) -> String {
106 let inverse_allergens: FastMap<_, _> =
107 input.allergens.iter().map(|(k, v)| (1 << v, k)).collect();
108 let mut todo: Vec<_> = input
109 .ingredients
110 .iter()
111 .filter_map(|(&k, &v)| (v.candidates != 0).then_some((k, v.candidates)))
112 .collect();
113 let mut done = BTreeMap::new();
114
115 // Eliminate known allergens from other ingredients.
116 while done.len() < todo.len() {
117 let mut mask = 0;
118
119 // There must be at least one ingredient with only one allergen.
120 for (name, candidates) in &todo {
121 if candidates.count_ones() == 1 {
122 let allergen = inverse_allergens[candidates];
123 done.insert(*allergen, *name);
124
125 mask |= candidates;
126 }
127 }
128
129 todo.iter_mut().for_each(|(_, candidates)| *candidates &= !mask);
130 }
131
132 // Sort by alphabetical order of the allergens.
133 done.into_values().collect::<Vec<_>>().join(",")
134}