advent_of_code/year2020/
day21.rs1use crate::input::Input;
2use std::collections::{HashMap, HashSet};
3
4pub fn solve(input: &Input) -> Result<String, String> {
5 let on_error = || "Invalid input";
6
7 let mut allergen_to_idx = HashMap::with_capacity(8);
8 let mut allergen_names = Vec::with_capacity(8);
9
10 let mut ingredient_to_idx = HashMap::with_capacity(200);
11 let mut ingredient_names = Vec::with_capacity(200);
12
13 let mut ingredient_occurences: Vec<u16> = Vec::with_capacity(200);
14 let mut allergen_to_possible_ingredients = Vec::with_capacity(8);
15
16 for line in input.text.lines() {
17 let mut line_parts = line.splitn(2, " (contains ");
18
19 let ingredients = line_parts.next().ok_or_else(on_error)?;
20 let mut current_ingredients = HashSet::new();
21 for ingredient_name in ingredients.split(' ') {
22 let num_ingredients = ingredient_to_idx.len();
23 let ingredient_id = *ingredient_to_idx
24 .entry(ingredient_name)
25 .or_insert(num_ingredients);
26
27 if ingredient_id == ingredient_occurences.len() {
28 ingredient_occurences.push(1);
29 ingredient_names.push(ingredient_name);
30 } else {
31 ingredient_occurences[ingredient_id] += 1;
32 }
33
34 current_ingredients.insert(ingredient_id);
35 }
36
37 if let Some(allergens) = line_parts.next().ok_or_else(on_error)?.strip_suffix(')') {
38 for allergen_name in allergens.split(", ") {
39 let num_allergens = allergen_to_idx.len();
40 let allergen_id = *allergen_to_idx
41 .entry(allergen_name)
42 .or_insert(num_allergens);
43 if allergen_id == allergen_names.len() {
44 allergen_names.push(allergen_name);
45 allergen_to_possible_ingredients.push(current_ingredients.clone());
46 } else {
47 let existing = &allergen_to_possible_ingredients[allergen_id];
48 allergen_to_possible_ingredients[allergen_id] = current_ingredients
49 .intersection(existing)
50 .copied()
51 .collect();
52 }
53 }
54 } else {
55 return Err(on_error().to_string());
56 }
57 }
58
59 if input.is_part_one() {
60 return Ok((0..ingredient_to_idx.len())
61 .filter_map(|ingredient_id| {
62 if allergen_to_possible_ingredients
63 .iter()
64 .any(|possible| possible.contains(&ingredient_id))
65 {
66 None
67 } else {
68 Some(u64::from(ingredient_occurences[ingredient_id]))
69 }
70 })
71 .sum::<u64>()
72 .to_string());
73 }
74
75 let mut identified_products = allergen_to_possible_ingredients
76 .iter()
77 .filter_map(|possibilities| {
78 if possibilities.len() == 1 {
79 possibilities.iter().next().copied()
80 } else {
81 None
82 }
83 })
84 .collect::<Vec<usize>>();
85
86 while let Some(product_id) = identified_products.pop() {
87 for possibilities in allergen_to_possible_ingredients.iter_mut() {
88 if possibilities.len() > 1
89 && possibilities.remove(&product_id)
90 && possibilities.len() == 1
91 {
92 let p = possibilities
93 .iter()
94 .next()
95 .ok_or_else(|| "Internal error".to_string())?;
96 identified_products.push(*p);
97 }
98 }
99 }
100
101 let mut ingredient_and_allergents = allergen_to_possible_ingredients
102 .iter()
103 .enumerate()
104 .map(
105 |(idx, possible_ingredients)| -> Result<(usize, usize), String> {
106 Ok((
107 *possible_ingredients
108 .iter()
109 .next()
110 .ok_or_else(|| "Internal error".to_string())?,
111 idx,
112 ))
113 },
114 )
115 .collect::<Result<Vec<_>, _>>()?;
116
117 ingredient_and_allergents.sort_unstable_by(|a, b| {
118 let a_allergen_name = allergen_names[a.1];
119 let b_allergen_name = allergen_names[b.1];
120 a_allergen_name.cmp(b_allergen_name)
121 });
122
123 Ok(ingredient_and_allergents
124 .iter()
125 .map(|&(ingredient_id, _)| ingredient_names[ingredient_id])
126 .collect::<Vec<_>>()
127 .join(","))
128}
129
130#[test]
131pub fn tests() {
132 use crate::input::{test_part_one, test_part_two};
133
134 let example = "mxmxvkd kfcds sqjhc nhms (contains dairy, fish)
135trh fvjkl sbzzf mxmxvkd (contains dairy)
136sqjhc fvjkl (contains soy)
137sqjhc mxmxvkd sbzzf (contains fish)";
138 test_part_one!(example => "5".to_string());
139 test_part_two!(example => "mxmxvkd,sqjhc,fvjkl".to_string());
140
141 let other_input = include_str!("day21_input_other.txt");
142 test_part_one!(other_input => "2724".to_string());
143 test_part_two!(other_input => "xlxknk,cskbmx,cjdmk,bmhn,jrmr,tzxcmr,fmgxh,fxzh".to_string());
144
145 let real_input = include_str!("day21_input.txt");
146 test_part_one!(real_input => "2317".to_string());
147 test_part_two!(real_input => "kbdgs,sqvv,slkfgq,vgnj,brdd,tpd,csfmb,lrnz".to_string());
148}