From e597cc24b3d3547f57bd77ae9a4be1fa057abc03 Mon Sep 17 00:00:00 2001 From: Anthony Oteri Date: Thu, 14 Dec 2023 17:15:57 -0500 Subject: [PATCH] Day 14 - Part 2 Signed-off-by: Anthony Oteri --- day-14/src/bin/part2a.rs | 21 +++ day-14/src/part2.rs | 391 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 day-14/src/bin/part2a.rs diff --git a/day-14/src/bin/part2a.rs b/day-14/src/bin/part2a.rs new file mode 100644 index 0000000..4536314 --- /dev/null +++ b/day-14/src/bin/part2a.rs @@ -0,0 +1,21 @@ +use day_14::part2::process; +use miette::Context; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[tracing::instrument] +fn main() -> miette::Result<()> { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + #[cfg(not(feature = "dhat-heap"))] + tracing_subscriber::fmt::init(); + + let file = include_str!("../../test-input.txt"); + let result = process(file).context("process part 1")?; + + println!("{}", result); + Ok(()) +} diff --git a/day-14/src/part2.rs b/day-14/src/part2.rs index 1ff3976..39b9a5b 100644 --- a/day-14/src/part2.rs +++ b/day-14/src/part2.rs @@ -1,18 +1,401 @@ use crate::error::AocError; +use itertools::Itertools; +use nom::{ + branch::alt, + character::complete, + character::complete::line_ending, + multi::{many1, separated_list1}, + IResult, +}; +use rayon::prelude::*; +use std::{cmp::Ordering, collections::BTreeSet}; -#[tracing::instrument] +fn parse(input: &str) -> IResult<&str, Vec>> { + separated_list1( + line_ending, + many1(alt(( + complete::char('#'), + complete::char('.'), + complete::char('O'), + ))), + )(input) +} + +fn partial_cmp(a: &char, b: &char) -> Option { + match (a, b) { + ('O', 'O') => Some(Ordering::Equal), + ('O', _) => Some(Ordering::Less), + (_, 'O') => Some(Ordering::Greater), + (_, _) => Some(Ordering::Equal), + } +} +fn partial_rev_cmp(a: &char, b: &char) -> Option { + match (a, b) { + ('#', 'O') => Some(Ordering::Greater), + ('O', 'O') => Some(Ordering::Equal), + ('O', _) => Some(Ordering::Greater), + (_, 'O') => Some(Ordering::Less), + (_, _) => Some(Ordering::Equal), + } +} + +fn custom_sort(input: &[char]) -> Vec { + input + .split_inclusive(|&c| c == '#') + .map(|chunk| { + let mut chunk = chunk.to_vec(); + chunk.sort_by(|a, b| partial_cmp(a, b).unwrap()); + chunk + }) + .collect_vec() + .concat() +} + +fn custom_rev_sort(input: &[char]) -> Vec { + input + .split_inclusive(|&c| c == '#') + .map(|chunk| { + let mut chunk = chunk.to_vec(); + chunk.sort_by(|a, b| partial_rev_cmp(a, b).unwrap()); + chunk + }) + .collect_vec() + .concat() +} + +#[memoize::memoize] +fn transpose(input: Vec>) -> Vec> { + let mut columns_iter_collection = input.iter().map(|line| line.iter()).collect::>(); + + std::iter::from_fn(move || { + let mut items = vec![]; + for iter in &mut columns_iter_collection { + match iter.next() { + Some(item) => { + items.push(*item); + } + None => return None, + } + } + Some(items) + }) + .collect::>>() +} + +#[memoize::memoize] +pub fn sort_north(input: Vec>) -> Vec> { + let mut columns = transpose(input); + columns = columns + .par_iter() + .map(|c| custom_sort(c)) + .collect::>(); + + transpose(columns) +} + +#[memoize::memoize] +pub fn sort_south(input: Vec>) -> Vec> { + let mut columns = transpose(input); + columns = columns + .par_iter() + .map(|c| custom_rev_sort(c)) + .collect::>(); + + transpose(columns) +} + +#[memoize::memoize] +pub fn sort_west(input: Vec>) -> Vec> { + input.par_iter().map(|c| custom_sort(c)).collect::>() +} + +#[memoize::memoize] +pub fn sort_east(input: Vec>) -> Vec> { + input + .par_iter() + .map(|c| custom_rev_sort(c)) + .collect::>() +} + +#[memoize::memoize] +pub fn cycle(input: Vec>) -> Vec> { + let north = sort_north(input); + let west = sort_west(north); + let south = sort_south(west); + sort_east(south) +} + +#[allow(dead_code)] +fn print_grid(grid: &Vec>) { + for row in grid { + for col in row { + print!("{}", col); + } + println!(); + } +} + +fn get_cycle_len(input: &[Vec]) -> (usize, usize) { + // Return lenght of the initial input and the cycle length. + + let mut seen: BTreeSet>> = BTreeSet::default(); + + let mut cycled = input.to_owned(); + loop { + cycled = cycle(cycled); + if seen.contains(&cycled) { + break; + } + seen.insert(cycled.clone()); + } + + let initial_cycle_count = seen.len(); + + // Repeat the logic 2x to eliminate the effect of the initial few + // permutations that are not part of the cycle. + seen.clear(); + + loop { + cycled = cycle(cycled); + if seen.contains(&cycled) { + break; + } + seen.insert(cycled.clone()); + } + + let cycle_count = initial_cycle_count - seen.len(); + + (cycle_count, initial_cycle_count - cycle_count) +} + +fn weight(input: &[Vec]) -> u64 { + input + .iter() + .rev() + .enumerate() + .map(|(i, row)| row.iter().filter(|&c| *c == 'O').count() as u64 * (i + 1) as u64) + .sum() +} + +#[tracing::instrument(skip(input))] pub fn process(input: &str) -> miette::Result { - Ok(0) + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let (initial_perms, cycle_len) = get_cycle_len(&grid); + + let mut cycled = grid.clone(); + for _ in 0..initial_perms { + cycled = cycle(cycled); + } + + let n = 1000000000; + let remainder = (n - initial_perms) % cycle_len; + + for _ in 0..remainder { + cycled = cycle(cycled); + } + + Ok(weight(&cycled)) } #[cfg(test)] mod tests { use super::*; + use rstest::rstest; + + #[test_log::test(rstest)] + #[case("O....#....", "O....#....")] // 1 + #[case("O....#....", "O....#....")] // 2 + #[case(".....##...", ".....##...")] // 3 + #[case("OO.#O....O", "OO.#OO....")] // 4 + #[case(".O.....O#.", "OO......#.")] // 5 + #[case("O.#..O.#.#", "O.#O...#.#")] // 6 + #[case("..O..#O..O", "O....#OO..")] // 7 + #[case(".......O..", "O.........")] // 8 + #[case("#....###..", "#....###..")] // 9 + #[case("#OO..#....", "#OO..#....")] // 10 + fn test_sorting_forward(#[case] input: &str, #[case] output: &str) { + let chars = input.chars().collect_vec(); + let sorted = custom_sort(&chars); + + assert_eq!(sorted, output.chars().collect_vec()); + } + + #[test_log::test(rstest)] + #[case("O...#....", "...O#....")] // 1 + #[case("O....#....", "....O#....")] // 2 + #[case(".....##...", ".....##...")] // 3 + #[case("OO.#O....O", ".OO#....OO")] // 4 + #[case(".O.....O#.", "......OO#.")] // 5 + #[case("O.#..O.#.#", ".O#...O#.#")] // 6 + #[case("..O..#O..O", "....O#..OO")] // 7 + #[case(".......O..", ".........O")] // 8 + #[case("#....###..", "#....###..")] // 9 + #[case("#OO..#....", "#..OO#....")] // 10 + fn test_sorting_reverse(#[case] input: &str, #[case] output: &str) { + let chars = input.chars().collect_vec(); + let sorted = custom_rev_sort(&chars); + + assert_eq!(sorted, output.chars().collect_vec()); + } #[test_log::test] - fn test_process() -> miette::Result<()> { + fn test_sort_north() { let input = include_str!("../test-input.txt"); - assert_eq!(0, process(input)?); + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let north = sort_north(grid); + + let expected = [ + "OOOO.#.O..", + "OO..#....#", + "OO..O##..O", + "O..#.OO...", + "........#.", + "..#....#.#", + "..O..#.O.O", + "..O.......", + "#....###..", + "#....#....", + ]; + + assert_eq!( + north, + expected + .iter() + .map(|s| s.chars().collect_vec()) + .collect_vec() + ); + } + + #[test_log::test] + fn test_sort_south() { + let input = include_str!("../test-input.txt"); + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let south = sort_south(grid); + let expected = [ + ".....#....", + "....#....#", + "...O.##...", + "...#......", + "O.O....O#O", + "O.#..O.#.#", + "O....#....", + "OO....OO..", + "#OO..###..", + "#OO.O#...O", + ]; + + assert_eq!( + south, + expected + .iter() + .map(|s| s.chars().collect_vec()) + .collect_vec() + ); + } + + #[test_log::test] + fn test_sort_east() { + let input = include_str!("../test-input.txt"); + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let east = sort_east(grid); + + let expected = [ + "....O#....", + ".OOO#....#", + ".....##...", + ".OO#....OO", + "......OO#.", + ".O#...O#.#", + "....O#..OO", + ".........O", + "#....###..", + "#..OO#....", + ]; + + assert_eq!( + east, + expected + .iter() + .map(|s| s.chars().collect_vec()) + .collect_vec() + ); + } + + #[test_log::test] + fn test_sort_west() { + let input = include_str!("../test-input.txt"); + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let west = sort_west(grid); + + let expected = [ + "O....#....", + "OOO.#....#", + ".....##...", + "OO.#OO....", + "OO......#.", + "O.#O...#.#", + "O....#OO..", + "O.........", + "#....###..", + "#OO..#....", + ]; + assert_eq!( + west, + expected + .iter() + .map(|s| s.chars().collect_vec()) + .collect_vec() + ); + } + + #[test_log::test] + fn test_cycle() { + let input = include_str!("../test-input.txt"); + let (input, grid) = parse(input).unwrap(); + debug_assert!(input.is_empty()); + + let cycle = cycle(grid); + + let expected = [ + ".....#....", + "....#...O#", + "...OO##...", + ".OO#......", + ".....OOO#.", + ".O#...O#.#", + "....O#....", + "......OOOO", + "#...O###..", + "#..OO#....", + ]; + + assert_eq!( + cycle, + expected + .iter() + .map(|s| s.chars().collect_vec()) + .collect_vec() + ); + } + + #[test_log::test(rstest)] + #[case("test-input.txt", 64)] + fn test_process(#[case] filename: &str, #[case] expected: u64) -> miette::Result<()> { + let input = + String::from_utf8_lossy(&std::fs::read(std::path::Path::new(filename)).unwrap()) + .parse::() + .unwrap(); + assert_eq!(expected, process(&input)?); Ok(()) } }