diff --git a/aoc2020/aoc2020_bench/benches/aoc2020_bench.rs b/aoc2020/aoc2020_bench/benches/aoc2020_bench.rs index b83d30a..a786c61 100644 --- a/aoc2020/aoc2020_bench/benches/aoc2020_bench.rs +++ b/aoc2020/aoc2020_bench/benches/aoc2020_bench.rs @@ -21,6 +21,7 @@ use aoc2020::day18; use aoc2020::day19; use aoc2020::day21; use aoc2020::day22; +use aoc2020::day23; fn aoc2020_all(c: &mut Criterion) { c.bench_function("day01", |b| b.iter(|| day01::run().unwrap())); @@ -44,6 +45,7 @@ fn aoc2020_all(c: &mut Criterion) { c.bench_function("day19", |b| b.iter(|| day19::run().unwrap())); c.bench_function("day21", |b| b.iter(|| day21::run().unwrap())); c.bench_function("day22", |b| b.iter(|| day22::run().unwrap())); + c.bench_function("day23", |b| b.iter(|| day23::run().unwrap())); } criterion_group! { diff --git a/aoc2020/input/day23.txt b/aoc2020/input/day23.txt new file mode 100644 index 0000000..79e342c --- /dev/null +++ b/aoc2020/input/day23.txt @@ -0,0 +1 @@ +315679824 diff --git a/aoc2020/input/day23_provided.txt b/aoc2020/input/day23_provided.txt new file mode 100644 index 0000000..ab40847 --- /dev/null +++ b/aoc2020/input/day23_provided.txt @@ -0,0 +1 @@ +389125467 diff --git a/aoc2020/src/day23.rs b/aoc2020/src/day23.rs new file mode 100644 index 0000000..93d6ceb --- /dev/null +++ b/aoc2020/src/day23.rs @@ -0,0 +1,156 @@ +use std::{collections::VecDeque, fmt::Write}; + +use anyhow::{Context, Result}; + +const INPUT: &str = include_str!("../input/day23.txt"); + +pub fn run() -> Result { + let mut res = String::with_capacity(128); + + writeln!(res, "part 1: {}", part1(INPUT)?)?; + + Ok(res) +} + +fn part1(input: &str) -> Result { + let mut cup_circle: CupCircle = input.parse()?; + + for _ in 0..100 { + cup_circle.step(); + } + + Ok(format!("{}", cup_circle)) +} + +struct CupCircle(VecDeque); + +/// CupCircle provides an abstraction over a VecDeque to emulate the steps of the game +/// +/// The "current" cup should always be the first one in the VecDeque at the end of a turn, for +/// convenience +/// +/// The Circle should never become empty if you only use its public interface, so all calls to +/// unwrap() in its implementation should never panic. +impl CupCircle { + pub fn front(&self) -> u64 { + *self.0.front().unwrap() + } + + fn pop_front(&mut self) -> u64 { + self.0.pop_front().unwrap() + } + + fn push_back(&mut self, value: u64) { + self.0.push_back(value); + } + + pub fn max(&self) -> u64 { + *self.0.iter().max().unwrap() + } + + fn remove_next_3(&mut self) -> [u64; 3] { + let first = self.pop_front(); + let second = self.pop_front(); + let third = self.pop_front(); + + [first, second, third] + } + + /// Shifts the cup circle, putting the first cup at the end of the deque + /// + /// This doesn't change anything to the cup circle layout, except that the new first cup is + /// considered the "current" cup at the end of a step in our implementation + fn shift(&mut self) { + let front = self.0.pop_front().unwrap(); + self.0.push_back(front); + } + + /// Executes one step of the game + pub fn step(&mut self) { + let current = self.front(); + // skip current + self.shift(); + + // The crab picks up the three cups that are immediately clockwise of the current cup. They + // are removed from the circle; cup spacing is adjusted as necessary to maintain the circle. + let removed_cups = self.remove_next_3(); + + // The crab selects a destination cup: the cup with a label equal to the current cup's label + // minus one. If this would select one of the cups that was just picked up, the crab will + // keep subtracting one until it finds a cup that wasn't just picked up. If at any point in + // this process the value goes below the lowest value on any cup's label, it wraps around to + // the highest value on any cup's label instead. + // + // TODO: use std::cmp::Ord::clamp when stabilized (Rust 1.50) + let mut destination = if current > 1 { current - 1 } else { self.max() }; + while removed_cups.contains(&destination) { + destination = if destination > 1 { + destination - 1 + } else { + self.max() + }; + } + + // place destination in front + while self.front() != destination { + self.shift(); + } + + // The crab places the cups it just picked up so that they are immediately clockwise of the + // destination cup. They keep the same order as when they were picked up. + // + // For this, let's put the destination at the end of the queue by shifting one last time + self.shift(); + removed_cups.iter().for_each(|cup| self.push_back(*cup)); + + // The crab selects a new current cup: the cup which is immediately clockwise of the current + // cup. + while self.front() != current { + self.shift(); + } + self.shift(); + } +} + +impl std::str::FromStr for CupCircle { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let circle = s + .trim_end() + .chars() + .map(|c| Ok(c.to_digit(10).context("character was not a digit")? as u64)) + .collect::>()?; + + Ok(CupCircle(circle)) + } +} + +impl std::fmt::Display for CupCircle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0 + .iter() + .cycle() + .skip_while(|cup| **cup != 1) + .skip(1) + .take(8) + .try_for_each(|cup| write!(f, "{}", cup)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const PROVIDED: &str = include_str!("../input/day23_provided.txt"); + + #[test] + fn part1_provided() { + assert_eq!(part1(PROVIDED).unwrap(), "67384529"); + } + + #[test] + fn part1_real() { + assert_eq!(part1(INPUT).unwrap(), "72496583"); + } +} diff --git a/aoc2020/src/lib.rs b/aoc2020/src/lib.rs index 54841ba..47f9f68 100644 --- a/aoc2020/src/lib.rs +++ b/aoc2020/src/lib.rs @@ -21,3 +21,4 @@ pub mod day18; pub mod day19; pub mod day21; pub mod day22; +pub mod day23; diff --git a/aoc2020/src/main.rs b/aoc2020/src/main.rs index 1629aff..896e808 100644 --- a/aoc2020/src/main.rs +++ b/aoc2020/src/main.rs @@ -23,6 +23,7 @@ use aoc2020::day18; use aoc2020::day19; use aoc2020::day21; use aoc2020::day22; +use aoc2020::day23; fn main() -> Result<()> { let days: &[DayFunc] = &[ @@ -47,6 +48,7 @@ fn main() -> Result<()> { day19::run, day21::run, day22::run, + day23::run, ]; aoc::run(days)