From d4e5f9783b9230968f94cb2a582ee6c63ded13f4 Mon Sep 17 00:00:00 2001 From: ictrobot Date: Sat, 7 Sep 2024 12:21:04 +0100 Subject: [PATCH] 2016 day 15 --- crates/utils/src/number.rs | 147 +++++++++++++++++++++++++++++++++++ crates/year2016/src/day15.rs | 76 ++++++++++++++++++ crates/year2016/src/lib.rs | 1 + 3 files changed, 224 insertions(+) create mode 100644 crates/year2016/src/day15.rs diff --git a/crates/utils/src/number.rs b/crates/utils/src/number.rs index 06d3823..87c03d9 100644 --- a/crates/utils/src/number.rs +++ b/crates/utils/src/number.rs @@ -3,6 +3,7 @@ //! Machine-dependent integer types aren't unsupported. use std::fmt::Debug; +use std::iter::{Product, Sum}; use std::ops::{ Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, Sub, SubAssign, @@ -14,6 +15,7 @@ pub trait Number: + Debug + Default + PartialEq + + PartialOrd + Add + AddAssign + Div @@ -24,12 +26,19 @@ pub trait Number: + RemAssign + Sub + SubAssign + + Sum + + for<'a> Sum<&'a Self> + + Product + + for<'a> Product<&'a Self> { const ZERO: Self; const ONE: Self; #[must_use] fn abs(self) -> Self; + + #[must_use] + fn rem_euclid(self, rhs: Self) -> Self; } /// Trait implemented by the primitive signed integer and floating point types. @@ -84,6 +93,11 @@ macro_rules! number_impl { fn abs(self) -> Self { self // no-op for unsigned integers } + + #[inline] + fn rem_euclid(self, rhs: Self) -> Self { + self.rem_euclid(rhs) + } })+ number_impl! {@common integer => $($t),+} @@ -99,6 +113,11 @@ macro_rules! number_impl { fn abs(self) -> Self { self.abs() } + + #[inline] + fn rem_euclid(self, rhs: Self) -> Self { + self.rem_euclid(rhs) + } })+ number_impl! {@common integer => $($t),+} @@ -115,6 +134,11 @@ macro_rules! number_impl { fn abs(self) -> Self { self.abs() } + + #[inline] + fn rem_euclid(self, rhs: Self) -> Self { + self.rem_euclid(rhs) + } })+ number_impl! {@common signed => $($t),+} @@ -152,3 +176,126 @@ macro_rules! number_impl { number_impl! {uint => u8, u16, u32, u64, u128} number_impl! {int => i8, i16, i32, i64, i128} number_impl! {float => f32, f64} + +/// Checks if the provided unsigned integer `n` is a prime number. +/// +/// # Examples +/// ``` +/// # use utils::number::is_prime; +/// assert_eq!(is_prime(7901u32), true); +/// assert_eq!(is_prime(2147483647u32), true); +/// assert_eq!(is_prime(4294967291u32), true); +/// assert_eq!(is_prime(6u32), false); +/// assert_eq!(is_prime(123u32), false); +/// ``` +#[inline] +pub fn is_prime(n: T) -> bool { + if n <= T::ONE { + return false; + } + if n == T::from(2) || n == T::from(3) { + return true; + } + if n % T::from(2) == T::ZERO || n % T::from(3) == T::ZERO { + return false; + } + + let mut i = T::from(5); + while let Some(square) = i.checked_mul(i) { + if square > n { + break; + } + + if n % i == T::ZERO || n % (i + T::from(2)) == T::ZERO { + return false; + } + + if let Some(next) = i.checked_add(T::from(6)) { + i = next; + } else { + break; + } + } + + true +} + +/// Computes the greatest common divisor (GCD) using the extended Euclidean algorithm. +/// +/// Returns a tuple `(gcd, x, y)` where `x`, `y` are the coefficients of Bézout's identity: +/// ```text +/// ax + by = gcd(a, b) +/// ``` +/// +/// # Examples +/// ``` +/// # use utils::number::egcd; +/// assert_eq!(egcd(252, 105), (21, -2, 5)); +/// assert_eq!((252 * -2) + (105 * 5), 21); +/// ``` +#[inline] +pub fn egcd(mut a: T, mut b: T) -> (T, T, T) { + let (mut x0, mut x1, mut y0, mut y1) = (T::ONE, T::ZERO, T::ZERO, T::ONE); + + while b != T::ZERO { + let q = a / b; + (a, b) = (b, a % b); + (x0, x1) = (x1, x0 - q * x1); + (y0, y1) = (y1, y0 - q * y1); + } + + (a, x0, y0) +} + +/// Computes the modular inverse of `a` modulo `b` if it exists. +/// +/// # Examples +/// ``` +/// # use utils::number::mod_inverse; +/// assert_eq!(mod_inverse(3, 5), Some(2)); +/// assert_eq!((3 * 2) % 5, 1); +/// +/// assert_eq!(mod_inverse(10, 23), Some(7)); +/// assert_eq!((10 * 7) % 23, 1); +/// +/// assert_eq!(mod_inverse(2, 8), None); +/// ``` +#[inline] +pub fn mod_inverse(a: T, b: T) -> Option { + let (gcd, x, _) = egcd(a, b); + if gcd == T::ONE { + Some(x.rem_euclid(b)) + } else { + None + } +} + +/// Solves a system of simultaneous congruences using the Chinese Remainder Theorem. +/// +/// This function finds the smallest non-negative integer `x` where `x % modulus = residue` for each +/// provided (residue, modulus) pair. +/// +/// # Examples +/// ``` +/// # use utils::number::chinese_remainder; +/// assert_eq!(chinese_remainder([1, 2, 3], [5, 7, 11]), Some(366)); +/// assert_eq!(366 % 5, 1); +/// assert_eq!(366 % 7, 2); +/// assert_eq!(366 % 11, 3); +/// ``` +#[inline] +pub fn chinese_remainder( + residues: impl IntoIterator, + moduli: impl IntoIterator, +) -> Option { + let moduli = moduli.into_iter(); + let product = moduli.clone().product(); + + let mut sum = T::ZERO; + for (residue, modulus) in residues.into_iter().zip(moduli) { + let p = product / modulus; + sum += residue * mod_inverse(p, modulus)? * p; + } + + Some(sum.rem_euclid(product)) +} diff --git a/crates/year2016/src/day15.rs b/crates/year2016/src/day15.rs new file mode 100644 index 0000000..8656eaa --- /dev/null +++ b/crates/year2016/src/day15.rs @@ -0,0 +1,76 @@ +use std::cell::RefCell; +use std::collections::HashSet; +use std::iter; +use utils::number::{chinese_remainder, is_prime}; +use utils::prelude::*; + +/// Finding when discs align. +/// +/// This puzzle is a system of linear simultaneous congruences which can be solved using +/// . +#[derive(Clone, Debug)] +pub struct Day15 { + discs: Vec, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +struct Disc { + size: u32, + position: u32, +} + +impl Day15 { + pub fn new(input: &str, _: InputType) -> Result { + let seen_positions: RefCell> = Default::default(); + Ok(Self { + discs: parser::u32() + .with_prefix(" has ".with_prefix(parser::u32()).with_prefix("Disc #")) + .then(parser::u32().with_prefix(" positions; at time=0, it is at position ")) + .with_suffix(".") + .map_res(|(size, position)| { + if position >= size { + Err("current position should be less than number of positions") + } else if !is_prime(size) { + Err("number of positions should be prime") + } else if !seen_positions.borrow_mut().insert(size) { + Err("number of positions should be unique") + } else { + Ok(Disc { size, position }) + } + }) + .parse_lines(input)?, + }) + } + + #[must_use] + pub fn part1(&self) -> i64 { + Self::earliest_alignment(self.discs.iter()) + } + + #[must_use] + pub fn part2(&self) -> i64 { + Self::earliest_alignment(self.discs.iter().chain(iter::once(&Disc { + size: 11, + position: 0, + }))) + } + + #[inline] + fn earliest_alignment<'a>(discs: impl Iterator + Clone) -> i64 { + let residues = discs + .clone() + .enumerate() + .map(|(i, disc)| -(disc.position as i64) - (i as i64 + 1)); + let moduli = discs.map(|disc| disc.size as i64); + + chinese_remainder(residues, moduli).expect("sizes are all prime") + } +} + +examples!(Day15 -> (i64, i64) [ + { + input: "Disc #1 has 5 positions; at time=0, it is at position 4.\n\ + Disc #2 has 2 positions; at time=0, it is at position 1.", + part1: 5 + }, +]); diff --git a/crates/year2016/src/lib.rs b/crates/year2016/src/lib.rs index eb64eaf..44e6d7c 100644 --- a/crates/year2016/src/lib.rs +++ b/crates/year2016/src/lib.rs @@ -16,4 +16,5 @@ utils::year!(2016 => year2016, ${ 12 => day12::Day12, 13 => day13::Day13, 14 => day14::Day14<'_>, + 15 => day15::Day15, });