diff --git a/2022/11/Cargo.lock b/2022/11/Cargo.lock new file mode 100644 index 0000000..5350235 --- /dev/null +++ b/2022/11/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "common" +version = "0.1.0" + +[[package]] +name = "day11" +version = "0.1.0" +dependencies = [ + "common", +] diff --git a/2022/11/Cargo.toml b/2022/11/Cargo.toml new file mode 100644 index 0000000..5f47442 --- /dev/null +++ b/2022/11/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "day11" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +common = { path = "../common" } diff --git a/2022/11/input b/2022/11/input new file mode 100644 index 0000000..54ae24d --- /dev/null +++ b/2022/11/input @@ -0,0 +1,55 @@ +Monkey 0: + Starting items: 97, 81, 57, 57, 91, 61 + Operation: new = old * 7 + Test: divisible by 11 + If true: throw to monkey 5 + If false: throw to monkey 6 + +Monkey 1: + Starting items: 88, 62, 68, 90 + Operation: new = old * 17 + Test: divisible by 19 + If true: throw to monkey 4 + If false: throw to monkey 2 + +Monkey 2: + Starting items: 74, 87 + Operation: new = old + 2 + Test: divisible by 5 + If true: throw to monkey 7 + If false: throw to monkey 4 + +Monkey 3: + Starting items: 53, 81, 60, 87, 90, 99, 75 + Operation: new = old + 1 + Test: divisible by 2 + If true: throw to monkey 2 + If false: throw to monkey 1 + +Monkey 4: + Starting items: 57 + Operation: new = old + 6 + Test: divisible by 13 + If true: throw to monkey 7 + If false: throw to monkey 0 + +Monkey 5: + Starting items: 54, 84, 91, 55, 59, 72, 75, 70 + Operation: new = old * old + Test: divisible by 7 + If true: throw to monkey 6 + If false: throw to monkey 3 + +Monkey 6: + Starting items: 95, 79, 79, 68, 78 + Operation: new = old + 3 + Test: divisible by 3 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 7: + Starting items: 61, 97, 67 + Operation: new = old + 4 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 5 diff --git a/2022/11/src/main.rs b/2022/11/src/main.rs new file mode 100644 index 0000000..04a2edb --- /dev/null +++ b/2022/11/src/main.rs @@ -0,0 +1,289 @@ +use std::str::FromStr; + +fn main() { + let input_file = common::parse_args_input_file(&mut std::env::args()); + let contents = std::fs::read_to_string(input_file).expect("Failed to read input file"); + let mut monkeys = Monkey::parse_monkeys(&contents); + let mut rounds = 20; + while rounds > 0 { + Monkey::do_round(&mut monkeys, 3, None); + rounds -= 1; + } + let mut inspection_counts = std::vec::Vec::::new(); + for (index, value) in monkeys.iter().enumerate() { + println!("Monkey {} inspected {} items", index, value.inspection_count); + inspection_counts.push(value.inspection_count); + } + inspection_counts.sort(); + println!("[PART 1] Monkey business value: {}", inspection_counts[inspection_counts.len()-1] * inspection_counts[inspection_counts.len()-2]); + + monkeys = Monkey::parse_monkeys(&contents); + // To keep numbers manageable, congruence is used. A modulo needs to be picked, + // During each round, items are tested against the test_divisor of the monkey. + // (X/z) ?≡ (Y/z) (mod N) is not always congruent in (mod N) space unless gcd(z, N) = 1 + // @see https://www.youtube.com/watch?v=_ge1zGADmWY + // + // In order to satisfy gcd(z, N) = 1 for all z where z is the test_divisor of the monkey, + // then N should be the least common multiple (LCM) of all z. + // I'm not actually sure the LCM(A, B, C) = LCM(LCM(A, B), C), ... so this might be + // working for the wrong reason (eg., the modulo created ends up being a simple multiple + // of all the test_divisors in the problem). It may also only be working since the test_divisors + // in the input are all already prime. + // + let mut modulo = 1; + for m in monkeys.iter() { + modulo = modulo * (m.test_divisor / gcd(modulo, m.test_divisor)); + } + println!("Modulo: {}", modulo); + + rounds = 10000; + while rounds > 0 { + println!("Round {}", 10000 - rounds); + Monkey::do_round(&mut monkeys, 1, Some(modulo)); + rounds -= 1; + } + inspection_counts.clear(); + for (index, value) in monkeys.iter().enumerate() { + println!("Monkey {} inspected {} items", index, value.inspection_count); + inspection_counts.push(value.inspection_count); + } + inspection_counts.sort(); + println!("[PART 2] (Using mod {}) Monkey business value: {}", modulo, inspection_counts[inspection_counts.len()-1] as f64 * inspection_counts[inspection_counts.len()-2] as f64); +} + +// Using binary GCD: https://en.wikipedia.org/wiki/Binary_GCD_algorithm +// Retrieved on 2022-12-11, modified to use i64 instead (but not supporting negative u, v). +pub fn gcd(mut u: i64, mut v: i64) -> i64 { + use std::cmp::min; + use std::mem::swap; + + assert!(u >= 0); + assert!(v >= 0); + + // Base cases: gcd(n, 0) = gcd(0, n) = n + if u == 0 { + return v; + } else if v == 0 { + return u; + } + + // Using identities 2 and 3: + // gcd(2ⁱ u, 2ʲ v) = 2ᵏ gcd(u, v) with u, v odd and k = min(i, j) + // 2ᵏ is the greatest power of two that divides both u and v + let i = u.trailing_zeros(); u >>= i; + let j = v.trailing_zeros(); v >>= j; + let k = min(i, j); + + loop { + // u and v are odd at the start of the loop + //debug_assert!(u % 2 == 1, "u = {} is even", u); + //debug_assert!(v % 2 == 1, "v = {} is even", v); + + // Swap if necessary so u <= v + if u > v { + swap(&mut u, &mut v); + } + // u and v are still both odd after (potentially) swapping + + // Using identity 4 (gcd(u, v) = gcd(|v-u|, min(u, v)) + v -= u; + // v is now even, but u is unchanged (and odd) + + // Identity 1: gcd(u, 0) = u + // The shift by k is necessary to add back the 2ᵏ factor that was removed before the loop + if v == 0 { + return u << k; + } + + // Identity 3: gcd(u, 2ʲ v) = gcd(u, v) (u is known to be odd) + v >>= v.trailing_zeros(); + // v is now odd again + } +} + +#[derive(Debug)] +struct Monkey { + items: std::vec::Vec, + update: String, + test_divisor: i64, + success_target: usize, + failure_target: usize, + inspection_count: u32, +} + +impl Monkey { + fn operation(&self, old: i64, modulo: Option) -> i64 { + let words: std::vec::Vec<&str> = self.update.split(' ').collect(); + let mut left = if words[0].eq("old") { + old + } else { + i64::from_str(words[0]).unwrap() + }; + if modulo.is_some() && left >= modulo.unwrap() { + left %= modulo.unwrap(); + } + let mut right = if words[2].eq("old") { + old + } else { + i64::from_str(words[2]).unwrap() + }; + if modulo.is_some() && right >= modulo.unwrap() { + right %= modulo.unwrap(); + } + let new = match words[1] { + "+" => { + let n = left + right; + if modulo.is_some() { + n % modulo.unwrap() + } + else { + n + } + }, + "*" => { + let n = left * right; + if modulo.is_some() { + let x = left as f64; + let c = ((x * (right as f64)) / (modulo.unwrap() as f64)) as i64; + let r = (left * right - c * modulo.unwrap()) % modulo.unwrap(); + if r < 0 { + r + modulo.unwrap() + } + else { + r + } + } + else { + n + } + }, + _ => { + unreachable!(); + } + }; + return new; + } + + fn parse_monkeys(contents: &String) -> std::vec::Vec { + let mut monkeys = std::vec::Vec::::new(); + let mut m = Monkey { + items: std::vec::Vec::::new(), + update: "".to_string(), + test_divisor: 1, + success_target: 0, + failure_target: 0, + inspection_count: 0, + }; + for line in contents.lines() { + if line.eq("") { + monkeys.push(m); + m = Monkey { + items: std::vec::Vec::::new(), + update: "".to_string(), + test_divisor: 1, + success_target: 0, + failure_target: 0, + inspection_count: 0, + }; + continue; + } + if line.trim().starts_with("Monkey ") { + continue; + } + let l = line.trim(); + let (_, value) = l.split_once(':').unwrap(); + if l.starts_with("Starting items: ") { + for item in value.split(',') { + m.items.push(i64::from_str(item.trim()).unwrap()); + } + } + else if l.starts_with("Operation: ") { + let (_, operation) = value.split_once('=').unwrap(); + m.update = operation.trim().to_string(); + } + else if l.starts_with("Test: ") { + let words: std::vec::Vec<&str> = value.trim().split(' ').collect(); + m.test_divisor = i64::from_str(words[2]).unwrap(); + } + else if l.starts_with("If true: ") { + let words: std::vec::Vec<&str> = value.trim().split(' ').collect(); + m.success_target = usize::from_str(words[3]).unwrap(); + } + else if l.starts_with("If false: ") { + let words: std::vec::Vec<&str> = value.trim().split(' ').collect(); + m.failure_target = usize::from_str(words[3]).unwrap(); + } + else { + unreachable!(); + } + } + monkeys.push(m); + return monkeys; + } + + fn do_round(monkeys: &mut std::vec::Vec, worry_divisor: i64, modulo: Option) { + for index in 0..monkeys.len() { + println!("Processing monkey {} ({} items)", index, monkeys[index].items.len()); + // Go through each item, starting with front of list + if monkeys[index].items.len() == 0 { + continue; + } + loop { + let mut item = monkeys[index].items.remove(0); + println!(" Inspecting item {}", item); + item = monkeys[index].operation(item, modulo); + println!(" Item updated to {}", item); + if worry_divisor != 1 { + item /= worry_divisor; + println!(" Got bored, item updated to {}", item); + } + monkeys[index].inspection_count += 1; + + // Test + println!(" Testing against value {}", monkeys[index].test_divisor); + let target = if item % monkeys[index].test_divisor == 0 { + monkeys[index].success_target + } + else { + monkeys[index].failure_target + }; + println!(" Item {} sent to monkey {}", item, target); + monkeys[target].items.push(item); + + if monkeys[index].items.len() == 0 { + break; + } + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + let monkeys = Monkey::parse_monkeys(&std::fs::read_to_string("test_input").unwrap()); + assert_eq!(monkeys.len(), 4); + println!("{:?}", monkeys); + } + + #[test] + fn operation() { + let mut monkey = Monkey { + items: vec![79, 98], + update: "old * 19".to_string(), + test_divisor: 23, + success_target: 2, + failure_target: 3, + inspection_count: 0, + }; + assert_eq!(monkey.operation(79, None), 1501); + monkey.update = "old + 6".to_string(); + assert_eq!(monkey.operation(54, None), 60); + monkey.update = "old * old".to_string(); + assert_eq!(monkey.operation(5, None), 25); + } +} diff --git a/2022/11/test_input b/2022/11/test_input new file mode 100644 index 0000000..c04eddb --- /dev/null +++ b/2022/11/test_input @@ -0,0 +1,27 @@ +Monkey 0: + Starting items: 79, 98 + Operation: new = old * 19 + Test: divisible by 23 + If true: throw to monkey 2 + If false: throw to monkey 3 + +Monkey 1: + Starting items: 54, 65, 75, 74 + Operation: new = old + 6 + Test: divisible by 19 + If true: throw to monkey 2 + If false: throw to monkey 0 + +Monkey 2: + Starting items: 79, 60, 97 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 3: + Starting items: 74 + Operation: new = old + 3 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 1 \ No newline at end of file