tangled
alpha
login
or
join now
jonas.tngl.sh
/
advent-of-code
0
fork
atom
Advent of Code solutions in Rust
0
fork
atom
overview
issues
pulls
pipelines
feat: day 21 of AoC 2020
jonas.tngl.sh
2 months ago
efb38cd8
6df651a8
+378
-2
3 changed files
expand all
collapse all
unified
split
aoc_2020
src
day21.rs
main.rs
aoc_utils
src
iter.rs
+320
aoc_2020/src/day21.rs
···
1
1
+
use std::{
2
2
+
borrow::Borrow,
3
3
+
collections::{BTreeMap, BTreeSet, HashMap},
4
4
+
};
5
5
+
6
6
+
use anyhow::bail;
7
7
+
use aoc_companion::prelude::*;
8
8
+
use aoc_utils::iter::IterUtils;
9
9
+
use itertools::Itertools as _;
10
10
+
11
11
+
pub(crate) struct Door<'a> {
12
12
+
foods: HashMap<BTreeSet<Ingredient<'a>>, BTreeSet<Allergen<'a>>>,
13
13
+
}
14
14
+
15
15
+
impl<'input> Solution<'input> for Door<'input> {
16
16
+
fn parse(input: &'input str) -> Result<Self> {
17
17
+
input
18
18
+
.lines()
19
19
+
.map(|line| {
20
20
+
let Some((ingredients, allergens)) = line.split_once(" (contains ") else {
21
21
+
bail!("missing allergens in food spec");
22
22
+
};
23
23
+
let Some(allergens) = allergens.strip_suffix(')') else {
24
24
+
bail!("missing closing parenthesis after allergen list");
25
25
+
};
26
26
+
Ok((
27
27
+
ingredients
28
28
+
.split_ascii_whitespace()
29
29
+
.map(Ingredient)
30
30
+
.collect(),
31
31
+
allergens.split(", ").map(Allergen).collect(),
32
32
+
))
33
33
+
})
34
34
+
.try_collect()
35
35
+
.map(|foods| Door { foods })
36
36
+
}
37
37
+
38
38
+
fn part1(&self) -> usize {
39
39
+
safe_ingredients(&self.foods).count()
40
40
+
}
41
41
+
42
42
+
fn part2(&self) -> String {
43
43
+
let inv_mapping: BTreeMap<_, _> = infer_mapping(&self.foods)
44
44
+
.into_iter()
45
45
+
.map(|(a, i)| (i, a))
46
46
+
.collect();
47
47
+
48
48
+
inv_mapping.values().join(",")
49
49
+
}
50
50
+
}
51
51
+
52
52
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53
53
+
struct Ingredient<'a>(&'a str);
54
54
+
55
55
+
impl std::fmt::Display for Ingredient<'_> {
56
56
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57
57
+
f.write_str(self.0)
58
58
+
}
59
59
+
}
60
60
+
61
61
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
62
62
+
struct Allergen<'a>(&'a str);
63
63
+
64
64
+
fn intersect<T: Copy + Ord>(other: &mut BTreeSet<T>, new: BTreeSet<T>) {
65
65
+
*other = other.intersection(&new).copied().collect();
66
66
+
}
67
67
+
68
68
+
fn infer_mapping<'a, Is, As>(
69
69
+
foods: impl IntoIterator<Item = (Is, As)>,
70
70
+
) -> HashMap<Ingredient<'a>, Allergen<'a>>
71
71
+
where
72
72
+
Is: Borrow<BTreeSet<Ingredient<'a>>> + Ord,
73
73
+
As: Borrow<BTreeSet<Allergen<'a>>> + Ord,
74
74
+
{
75
75
+
let mut relations = foods
76
76
+
.into_iter()
77
77
+
.map(|(i, a)| (a.borrow().clone(), i.borrow().clone()))
78
78
+
.btree_merge(intersect);
79
79
+
80
80
+
let mut solution = HashMap::new();
81
81
+
82
82
+
while !relations.is_empty() {
83
83
+
dbg!(relations.len());
84
84
+
// Generate non-empty intersections
85
85
+
let mut intersections = relations
86
86
+
.iter()
87
87
+
.tuple_combinations()
88
88
+
.filter_map(|((lhs_a, lhs_i), (rhs_a, rhs_i))| {
89
89
+
let int_a: BTreeSet<_> = lhs_a.intersection(rhs_a).copied().collect();
90
90
+
if int_a.is_empty() {
91
91
+
return None;
92
92
+
}
93
93
+
let int_i = lhs_i.intersection(rhs_i).copied().collect();
94
94
+
Some((int_a, int_i))
95
95
+
})
96
96
+
.btree_merge(intersect);
97
97
+
relations.append(&mut intersections);
98
98
+
99
99
+
// Identify and eliminate unique relations
100
100
+
while let Some((allergen, ingredient)) =
101
101
+
relations.iter().find_map(|(allergens, ingredients)| {
102
102
+
let allergen = allergens.iter().exactly_one().ok()?;
103
103
+
let ingredient = ingredients
104
104
+
.iter()
105
105
+
.filter(|&i| !solution.contains_key(i))
106
106
+
.exactly_one()
107
107
+
.ok()?;
108
108
+
Some((*allergen, *ingredient))
109
109
+
})
110
110
+
{
111
111
+
solution.insert(ingredient, allergen);
112
112
+
113
113
+
relations = relations
114
114
+
.into_iter()
115
115
+
.map(|(mut allergens, mut ingredients)| {
116
116
+
allergens.remove(&allergen);
117
117
+
ingredients.remove(&ingredient);
118
118
+
(allergens, ingredients)
119
119
+
})
120
120
+
.filter(|(allergens, _)| !allergens.is_empty())
121
121
+
.btree_merge(intersect);
122
122
+
}
123
123
+
}
124
124
+
125
125
+
solution
126
126
+
}
127
127
+
128
128
+
fn safe_ingredients<'a>(
129
129
+
foods: &HashMap<BTreeSet<Ingredient<'a>>, BTreeSet<Allergen<'a>>>,
130
130
+
) -> impl Iterator<Item = Ingredient<'a>> {
131
131
+
let mapping = infer_mapping(foods);
132
132
+
foods
133
133
+
.keys()
134
134
+
.flatten()
135
135
+
.copied()
136
136
+
.filter(move |ingredient| !mapping.contains_key(ingredient))
137
137
+
}
138
138
+
139
139
+
#[cfg(test)]
140
140
+
mod tests {
141
141
+
use std::collections::HashMap;
142
142
+
143
143
+
use super::*;
144
144
+
145
145
+
const EXAMPLE_INPUT: &str = "\
146
146
+
mxmxvkd kfcds sqjhc nhms (contains dairy, fish)
147
147
+
trh fvjkl sbzzf mxmxvkd (contains dairy)
148
148
+
sqjhc fvjkl (contains soy)
149
149
+
sqjhc mxmxvkd sbzzf (contains fish)";
150
150
+
151
151
+
const EXAMPLE_FOODS: [(&[Ingredient], &[Allergen]); 4] = [
152
152
+
(
153
153
+
&[
154
154
+
Ingredient("mxmxvkd"),
155
155
+
Ingredient("kfcds"),
156
156
+
Ingredient("sqjhc"),
157
157
+
Ingredient("nhms"),
158
158
+
],
159
159
+
&[Allergen("dairy"), Allergen("fish")],
160
160
+
),
161
161
+
(
162
162
+
&[
163
163
+
Ingredient("trh"),
164
164
+
Ingredient("fvjkl"),
165
165
+
Ingredient("sbzzf"),
166
166
+
Ingredient("mxmxvkd"),
167
167
+
],
168
168
+
&[Allergen("dairy")],
169
169
+
),
170
170
+
(
171
171
+
&[Ingredient("sqjhc"), Ingredient("fvjkl")],
172
172
+
&[Allergen("soy")],
173
173
+
),
174
174
+
(
175
175
+
&[
176
176
+
Ingredient("sqjhc"),
177
177
+
Ingredient("mxmxvkd"),
178
178
+
Ingredient("sbzzf"),
179
179
+
],
180
180
+
&[Allergen("fish")],
181
181
+
),
182
182
+
];
183
183
+
184
184
+
const EXAMPLE_MAPPING: [(Ingredient, Allergen); 3] = [
185
185
+
(Ingredient("mxmxvkd"), Allergen("dairy")),
186
186
+
(Ingredient("sqjhc"), Allergen("fish")),
187
187
+
(Ingredient("fvjkl"), Allergen("soy")),
188
188
+
];
189
189
+
190
190
+
const CUSTOM_FOODS: [(&[Ingredient], &[Allergen]); 6] = [
191
191
+
(
192
192
+
&[
193
193
+
Ingredient("A"),
194
194
+
Ingredient("B"),
195
195
+
Ingredient("C"),
196
196
+
Ingredient("D"),
197
197
+
Ingredient("E"),
198
198
+
Ingredient("F"),
199
199
+
Ingredient("G"),
200
200
+
],
201
201
+
&[Allergen("fish"), Allergen("nuts"), Allergen("eggs")],
202
202
+
),
203
203
+
(
204
204
+
&[
205
205
+
Ingredient("A"),
206
206
+
Ingredient("B"),
207
207
+
Ingredient("D"),
208
208
+
Ingredient("E"),
209
209
+
],
210
210
+
&[Allergen("fish"), Allergen("nuts")],
211
211
+
),
212
212
+
(
213
213
+
&[
214
214
+
Ingredient("A"),
215
215
+
Ingredient("B"),
216
216
+
Ingredient("C"),
217
217
+
Ingredient("F"),
218
218
+
],
219
219
+
&[Allergen("fish"), Allergen("eggs")],
220
220
+
),
221
221
+
(
222
222
+
&[
223
223
+
Ingredient("A"),
224
224
+
Ingredient("C"),
225
225
+
Ingredient("D"),
226
226
+
Ingredient("G"),
227
227
+
],
228
228
+
&[Allergen("fish"), Allergen("plutonium")],
229
229
+
),
230
230
+
(
231
231
+
&[
232
232
+
Ingredient("B"),
233
233
+
Ingredient("C"),
234
234
+
Ingredient("D"),
235
235
+
Ingredient("F"),
236
236
+
Ingredient("G"),
237
237
+
],
238
238
+
&[Allergen("nuts"), Allergen("plutonium")],
239
239
+
),
240
240
+
(
241
241
+
&[Ingredient("C"), Ingredient("D"), Ingredient("E")],
242
242
+
&[Allergen("eggs"), Allergen("plutonium")],
243
243
+
),
244
244
+
];
245
245
+
246
246
+
const CUSTOM_MAPPING: [(Ingredient, Allergen); 4] = [
247
247
+
(Ingredient("A"), Allergen("fish")),
248
248
+
(Ingredient("B"), Allergen("nuts")),
249
249
+
(Ingredient("C"), Allergen("eggs")),
250
250
+
(Ingredient("D"), Allergen("plutonium")),
251
251
+
];
252
252
+
253
253
+
#[test]
254
254
+
fn parse_example_input() {
255
255
+
let Door { foods } = Door::parse(EXAMPLE_INPUT).unwrap();
256
256
+
assert_eq!(
257
257
+
foods,
258
258
+
HashMap::from_iter(EXAMPLE_FOODS.iter().map(|(k, v)| (
259
259
+
BTreeSet::from_iter(k.iter().copied()),
260
260
+
BTreeSet::from_iter(v.iter().copied())
261
261
+
)))
262
262
+
);
263
263
+
}
264
264
+
265
265
+
#[test]
266
266
+
fn infer_example_mapping() {
267
267
+
let foods = EXAMPLE_FOODS.iter().map(|(k, v)| {
268
268
+
(
269
269
+
BTreeSet::from_iter(k.iter().copied()),
270
270
+
BTreeSet::from_iter(v.iter().copied()),
271
271
+
)
272
272
+
});
273
273
+
assert_eq!(infer_mapping(foods), HashMap::from(EXAMPLE_MAPPING));
274
274
+
}
275
275
+
276
276
+
#[test]
277
277
+
fn infer_custom_example_mapping() {
278
278
+
let foods = CUSTOM_FOODS.iter().map(|(k, v)| {
279
279
+
(
280
280
+
BTreeSet::from_iter(k.iter().copied()),
281
281
+
BTreeSet::from_iter(v.iter().copied()),
282
282
+
)
283
283
+
});
284
284
+
assert_eq!(infer_mapping(foods), HashMap::from(CUSTOM_MAPPING));
285
285
+
}
286
286
+
287
287
+
#[test]
288
288
+
fn list_safe_example_ingredients() {
289
289
+
let foods = HashMap::from_iter(EXAMPLE_FOODS.iter().map(|(k, v)| {
290
290
+
(
291
291
+
BTreeSet::from_iter(k.iter().copied()),
292
292
+
BTreeSet::from_iter(v.iter().copied()),
293
293
+
)
294
294
+
}));
295
295
+
assert_eq!(
296
296
+
BTreeSet::from_iter(safe_ingredients(&foods)),
297
297
+
BTreeSet::from([
298
298
+
Ingredient("kfcds"),
299
299
+
Ingredient("nhms"),
300
300
+
Ingredient("sbzzf"),
301
301
+
Ingredient("sbzzf"),
302
302
+
Ingredient("trh"),
303
303
+
])
304
304
+
)
305
305
+
}
306
306
+
307
307
+
#[test]
308
308
+
fn list_safe_custom_example_ingredients() {
309
309
+
let foods = HashMap::from_iter(CUSTOM_FOODS.iter().map(|(k, v)| {
310
310
+
(
311
311
+
BTreeSet::from_iter(k.iter().copied()),
312
312
+
BTreeSet::from_iter(v.iter().copied()),
313
313
+
)
314
314
+
}));
315
315
+
assert_eq!(
316
316
+
BTreeSet::from_iter(safe_ingredients(&foods)),
317
317
+
BTreeSet::from([Ingredient("E"), Ingredient("F"), Ingredient("G"),])
318
318
+
)
319
319
+
}
320
320
+
}
+2
-2
aoc_2020/src/main.rs
···
20
20
mod day18;
21
21
mod day19;
22
22
mod day20;
23
23
-
// mod day21;
23
23
+
mod day21;
24
24
// mod day22;
25
25
// mod day23;
26
26
// mod day24;
···
51
51
door!(2020-12-18 ~> day18),
52
52
door!(2020-12-19 ~> day19),
53
53
door!(2020-12-20 ~> day20),
54
54
-
// door!(2020-12-21 ~> day21),
54
54
+
door!(2020-12-21 ~> day21),
55
55
// door!(2020-12-22 ~> day22),
56
56
// door!(2020-12-23 ~> day23),
57
57
// door!(2020-12-24 ~> day24),
+56
aoc_utils/src/iter.rs
···
1
1
use std::{
2
2
+
collections::{BTreeMap, HashMap},
2
3
iter::{FusedIterator, Sum},
3
4
rc::Rc,
4
5
};
···
35
36
fallback_elem: Some(fallback_elem),
36
37
}
37
38
}
39
39
+
40
40
+
fn btree_merge<K, V>(self, mut merge_fn: impl FnMut(&mut V, V)) -> BTreeMap<K, V>
41
41
+
where
42
42
+
Self: Sized + Iterator<Item = (K, V)>,
43
43
+
K: Ord,
44
44
+
{
45
45
+
self.fold(BTreeMap::new(), |mut map, (k, v)| {
46
46
+
use std::collections::btree_map::Entry::*;
47
47
+
match map.entry(k) {
48
48
+
Vacant(vacant_entry) => {
49
49
+
vacant_entry.insert(v);
50
50
+
}
51
51
+
Occupied(mut occupied_entry) => merge_fn(occupied_entry.get_mut(), v),
52
52
+
};
53
53
+
54
54
+
map
55
55
+
})
56
56
+
}
57
57
+
58
58
+
fn hash_merge<K, V>(self, mut merge_fn: impl FnMut(&mut V, V)) -> HashMap<K, V>
59
59
+
where
60
60
+
Self: Sized + Iterator<Item = (K, V)>,
61
61
+
K: std::hash::Hash + Eq,
62
62
+
{
63
63
+
self.fold(HashMap::new(), |mut map, (k, v)| {
64
64
+
use std::collections::hash_map::Entry::*;
65
65
+
match map.entry(k) {
66
66
+
Vacant(vacant_entry) => {
67
67
+
vacant_entry.insert(v);
68
68
+
}
69
69
+
Occupied(mut occupied_entry) => merge_fn(occupied_entry.get_mut(), v),
70
70
+
};
71
71
+
72
72
+
map
73
73
+
})
74
74
+
}
38
75
}
39
76
40
77
impl<T> IterUtils for T where T: Iterator {}
···
147
184
148
185
#[cfg(test)]
149
186
mod tests {
187
187
+
use itertools::Itertools;
188
188
+
150
189
use super::*;
151
190
152
191
#[test]
···
198
237
itertools::assert_equal(Few::<i32, 1>::default(), []);
199
238
itertools::assert_equal(Few::<i32, 2>::default(), []);
200
239
itertools::assert_equal(Few::<i32, 3>::default(), []);
240
240
+
}
241
241
+
242
242
+
#[test]
243
243
+
fn merge_by_counts() {
244
244
+
let pi_str = format!("{:.15}", core::f64::consts::PI);
245
245
+
let digits_of_pi = pi_str.chars().filter_map(|c| c.to_digit(10));
246
246
+
let btree_map = digits_of_pi
247
247
+
.clone()
248
248
+
.map(|d| (d, 1))
249
249
+
.btree_merge(|c, i| *c += i);
250
250
+
let hash_map = digits_of_pi
251
251
+
.clone()
252
252
+
.map(|d| (d, 1))
253
253
+
.hash_merge(|c, i| *c += i);
254
254
+
let counts = digits_of_pi.counts();
255
255
+
assert_eq!(hash_map, counts);
256
256
+
itertools::assert_equal(btree_map, hash_map.into_iter().sorted());
201
257
}
202
258
}