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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/* Copyright 2022 Mario Finelli
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Advent of Code 2022 Day 11: <https://adventofcode.com/2022/day/11>
//!
//! I thought that part one of this challenge was relatively easy and was at
//! first surprised that part two was just to increase the number of rounds
//! and remove a division step. Then I ran into my first overflow, increased
//! my integers to `u64` and then `u128` and they were still too small. I
//! thought that the prompt's instructions "you'll need to find another way to
//! keep your worry levels manageable" was just flavor text. I started to
//! implement a crate that added support for `u256`+ sized integers until I
//! decided that I was missing something. I wasn't really sure how to approach
//! the problem until I read something really helpful on
//! [reddit](https://www.reddit.com/r/adventofcode/comments/zim5o6/comment/izrr6mq/):
//!
//! >Hint: you don't care what the result of the division is, you just care
//! >whether the current worry level is divisible by the monkey's test value.
//! >Is there a way you can keep the worry level small(er) while still being
//! >able to tell if it's divisible?
//!
//! I played around some trying to divide the original factor until I found
//! the modulo method. During my cleanup phase I also read that it's better to
//! compute the least common multiple and use that instead of the product, but
//! reading around some more it seems that may be bad advice. I already
//! implemented it again using the LCM and the tests still pass so it can't
//! _hurt_, but I don't understand the math well enough to know for sure one
//! way or the other.
//!
//! What follows is my attempt to explain why/how the modulo method is needed
//! and works. Inspecting the monkeys we can see that some (or rather one) of
//! them multiplies the item value by itself (i.e., `value^2`) each time that
//! it inspects an object (it must be _really_ careless). In part one we have
//! a low enough number of rounds that reducing the worry factor by three each
//! time is enough to keep this value from getting too out of hand. It also
//! probably wouldn't be a problem if we never had the exponential growth and
//! only multiplied (or added) by set, fixed values. In any case, as it is the
//! item values can grow crazy big crazy fast. The strategy then is to compute
//! a value that we can remove from the item's value that will reduce its size
//! but keep in tact whether it's divisible by all of the monkeys' test values.
//! Consider some smaller, arbitrary numbers for the tests: `3`, `5`, and `7`.
//! Let's assume that we have an item with value `300` and that our overflow
//! is small say `500`. If we have a monkey that multiplies the item value by
//! itself we'll end up with a worry score of 90000 which would obviously
//! overflow. We also want to know whether the result is divisible by our
//! initial numbers and if not what the remainder is. `90000 % 3` and `90000 %
//! 5` are both `0`, but `90000 % 7` is `1`. We can "take out" `857` copies of
//! our LCM `105` (`857 * 105 == 89985`) and be left with `15`. `15 % 3` and
//! `15 % 5` are both `0` and `15 % 7` is `1`, just like our original, huge
//! number. We can safely replace that value with `15` which fits inside our
//! overflow. There are some much better, math filled explanations on the
//! Advent of Code subreddit that explain why/how this all works should you be
//! interested.

use crate::util;
use std::collections::VecDeque;

/// OperationType is a representation of how the worry level changes, we can
/// either add/multiply by another value or add/multiply the same value. This
/// lets store the strategy for each monkey in a way that will let us `match`
/// later.
#[derive(Debug)]
enum OperationType {
    Addition,
    AdditionSelf,
    Multiplication,
    MultiplicationSelf,
}

/// Monkey is the current state of each monkey, how many item inspections that
/// they have done, which items they're currently holding, which worry level
/// strategy they implement (and the value modifier if it's not *Self), as
/// well as the information necessary for determining how and to which monkey
/// it will pass items.
#[derive(Debug)]
struct Monkey {
    inspections: u64,
    items: VecDeque<u64>,
    operation_type: OperationType,
    operation_value: u32,
    test: u32,
    if_true: usize,
    if_false: usize,
}

/// The solution for the day eleven challenge.
///
/// There are three input arguments: the input (monkey definitions) as a
/// string, the number of rounds to simulate as an integer, and a boolean
/// whether or not we should implement the "static relief" method (i.e., divide
/// the worry by `3` on each inspection). If it's `false` then we do part two
/// of the challenge where we reduce the worry using the modulo method.
///
/// We start by parsing the input. Each monkey's definition is the same: it
/// takes six lines (seven including a trailing newline, so we split the input
/// into chunks of seven lines to parse it). The first line is the monkey's
/// "name" or index, starting with `0`. The next line is a comma-separated
/// list of the items/worry that a monkey is currently holding. The next line
/// is the definition of how the worry changes each time the monkey inspects
/// an item. There are four possibilities: the monkey can add or multiply by
/// a static number or they monkey can add the item's value to itself or
/// multiply the item's value by itself. Finally, are three lines that tell
/// the monkey where to send the item next. The first of these three lines is
/// the number to divide the item by and if the result is `0` (i.e., no
/// remainder) then the monkey gives the item to the monkey on the second line
/// but otherwise (i.e., there _is_ a remainder) then the monkey gives the
/// item to the monkey on the third line.
///
/// Parsing this input essentially amounts to splitting a bunch of whitespace
/// and parsing the integers found in certain indexes of the result. One
/// important implementation note is that the monkeys inspect the items in a
/// FIFO (first-in-first-out) order meaning that a standard vector is
/// unsuitable for tracking the items that the monkey is holding. We instead
/// use a [`std::collections::VecDeque`] and its `push_back()` and
/// `pop_front()` methods to achieve this behavior.
///
/// Then we start simulating the rounds. We check each item that the monkey
/// is holding (`pop()`ping them from the items list) and apply the worry
/// modification to it. We increase the number of inspections that the monkey
/// has done and then apply a worry relief strategy. We then check if the
/// item is divisible by the monkey's `test` number and then pass the item to
/// the corresponding monkey.
///
/// Finally, we sort the monkeys by how many inspections they did and return
/// the number of inspections made by the top two monkeys multiplied together.
///
/// # Example
/// ```rust
/// # use aoc::y22d11::y22d11;
/// // probably read this from the input file...
/// let input = concat!(
///     "Monkey 0:\n",
///     "  Starting items: 1, 2\n",
///     "  Operation: new = old * 8\n",
///     "  Test: divisible by 3\n",
///     "    If true: throw to monkey 2\n",
///     "    If false: throw to monkey 1\n",
///     "\n",
///     "Monkey 1:\n",
///     "  Starting items: 3, 4, 5, 6\n",
///     "  Operation: new = old + 9\n",
///     "  Test: divisible by 5\n",
///     "    If true: throw to monkey 0\n",
///     "    If false: throw to monkey 2\n",
///     "\n",
///     "Monkey 2:\n",
///     "  Starting items: 7\n",
///     "  Operation: new = old * old\n",
///     "  Test: divisible by 7\n",
///     "    If true: throw to monkey 1\n",
///     "    If false: throw to monkey 0",
/// );
/// assert_eq!(y22d11(&input, 5, true), 858);
/// assert_eq!(y22d11(&input, 5, false), 896);
/// ```
pub fn y22d11(input: &str, rounds: u32, static_relief: bool) -> u64 {
    let lines: Vec<_> = input.lines().collect();
    let mut monkeys = Vec::new();

    for monkey_defn in lines.chunks(7) {
        let mut items = VecDeque::new();

        // split_whitespace() automatically strips leading whitespace
        let items_parts: Vec<_> = monkey_defn[1].split_whitespace().collect();
        if items_parts.len() > 2 {
            for item in items_parts.iter().skip(2) {
                items.push_back(item.trim_end_matches(',').parse().unwrap());
            }
        }

        let operation_parts: Vec<_> =
            monkey_defn[2].split_whitespace().collect();
        let operation_type: OperationType;
        let operation_value: u32;

        if operation_parts[5] == "old" {
            operation_value = 0;

            if operation_parts[4] == "+" {
                operation_type = OperationType::AdditionSelf;
            } else {
                operation_type = OperationType::MultiplicationSelf;
            }
        } else {
            operation_value = operation_parts[5].parse().unwrap();

            if operation_parts[4] == "+" {
                operation_type = OperationType::Addition;
            } else {
                operation_type = OperationType::Multiplication;
            }
        }

        let test_parts: Vec<_> = monkey_defn[3].split_whitespace().collect();
        let true_parts: Vec<_> = monkey_defn[4].split_whitespace().collect();
        let false_parts: Vec<_> = monkey_defn[5].split_whitespace().collect();

        monkeys.push(Monkey {
            inspections: 0,
            items,
            operation_type,
            operation_value,
            test: test_parts[3].parse().unwrap(),
            if_true: true_parts[5].parse().unwrap(),
            if_false: false_parts[5].parse().unwrap(),
        });
    }

    let mut lcm = 1;
    for monkey in &monkeys {
        lcm = util::lcm(lcm, u64::from(monkey.test));
    }

    for _ in 0..rounds {
        for monkey_index in 0..monkeys.len() {
            // if the monkey doesn't have any items then their turn is over
            if monkeys[monkey_index].items.is_empty() {
                continue;
            }

            while let Some(mut item) = monkeys[monkey_index].items.pop_front() {
                // first the monkey inspects the item
                match monkeys[monkey_index].operation_type {
                    OperationType::AdditionSelf => item += item,
                    OperationType::Addition => {
                        item +=
                            u64::from(monkeys[monkey_index].operation_value);
                    }
                    OperationType::MultiplicationSelf => item *= item,
                    OperationType::Multiplication => {
                        item *=
                            u64::from(monkeys[monkey_index].operation_value);
                    }
                }

                // increment the monkey's inspection counter
                monkeys[monkey_index].inspections += 1;

                // implement the relief algorithm
                if static_relief {
                    item /= 3;
                } else if item > lcm {
                    item %= lcm;
                }

                // finally, throw (assign) the item to a new monkey
                if item % u64::from(monkeys[monkey_index].test) == 0 {
                    let new_monkey_index = monkeys[monkey_index].if_true;
                    monkeys[new_monkey_index].items.push_back(item);
                } else {
                    let new_monkey_index = monkeys[monkey_index].if_false;
                    monkeys[new_monkey_index].items.push_back(item);
                }
            }
        }
    }

    monkeys.sort_by(|a, b| a.inspections.cmp(&b.inspections));
    monkeys.pop().unwrap().inspections * monkeys.pop().unwrap().inspections
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn it_works() {
        let input = concat!(
            "Monkey 0:\n",
            "  Starting items: 79, 98\n",
            "  Operation: new = old * 19\n",
            "  Test: divisible by 23\n",
            "    If true: throw to monkey 2\n",
            "    If false: throw to monkey 3\n",
            "\n",
            "Monkey 1:\n",
            "  Starting items: 54, 65, 75, 74\n",
            "  Operation: new = old + 6\n",
            "  Test: divisible by 19\n",
            "    If true: throw to monkey 2\n",
            "    If false: throw to monkey 0\n",
            "\n",
            "Monkey 2:\n",
            "  Starting items: 79, 60, 97\n",
            "  Operation: new = old * old\n",
            "  Test: divisible by 13\n",
            "    If true: throw to monkey 1\n",
            "    If false: throw to monkey 3\n",
            "\n",
            "Monkey 3:\n",
            "  Starting items: 74\n",
            "  Operation: new = old + 3\n",
            "  Test: divisible by 17\n",
            "    If true: throw to monkey 0\n",
            "    If false: throw to monkey 1\n",
        );

        assert_eq!(y22d11(input, 20, true), 10605);
        assert_eq!(y22d11(input, 1000, false), 27019168);
        assert_eq!(y22d11(input, 10000, false), 2713310158);
    }

    #[test]
    fn the_solution() {
        let contents = fs::read_to_string("input/2022/day11.txt").unwrap();

        assert_eq!(y22d11(&contents, 20, true), 50830);
        assert_eq!(y22d11(&contents, 10000, false), 14399640002);
    }
}