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