diff --git a/2023/Cargo.lock b/2023/Cargo.lock index 0325d59..ca9ecec 100644 --- a/2023/Cargo.lock +++ b/2023/Cargo.lock @@ -118,6 +118,7 @@ version = "2023.0.0" dependencies = [ "itertools", "nom", + "rstest", ] [[package]] diff --git a/2023/day-12/Cargo.toml b/2023/day-12/Cargo.toml index 4f83799..1bf159b 100644 --- a/2023/day-12/Cargo.toml +++ b/2023/day-12/Cargo.toml @@ -10,3 +10,6 @@ repository.workspace = true [dependencies] nom = { workspace = true } itertools = {workspace = true } + +[dev-dependencies] +rstest = {workspace = true} diff --git a/2023/day-12/src/part1.rs b/2023/day-12/src/part1.rs index a7db908..668236b 100644 --- a/2023/day-12/src/part1.rs +++ b/2023/day-12/src/part1.rs @@ -1,20 +1,155 @@ #![warn(clippy::all, clippy::pedantic)] +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete, + multi::{many1, separated_list1}, + sequence::separated_pair, + IResult, Parser, +}; + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy)] +enum SpringStatus { + Working, + Failing, + Unknown, +} + +struct Row { + springs: Vec, + broken_spans: Vec, +} + +impl Row { + fn process(&self) -> usize { + let num_broken = self.broken_spans.iter().sum(); + let row_len = self.springs.len(); + let max_perm = 1_u32 << row_len; + (1..max_perm) + .filter(|x| x.count_ones() == num_broken) + .map(|x| { + let mut perm = Vec::new(); + (0..row_len).map(|y| 1 << y).for_each(|y| { + if y & x == 0 { + perm.push(SpringStatus::Working); + } else { + perm.push(SpringStatus::Failing); + } + }); + perm + }) + .filter(|x| { + self.springs + .iter() + .zip(x.iter()) + .all(|(a, b)| (a == b || *a == SpringStatus::Unknown)) + }) + .filter(|x| { + let (mut array, last, current_run) =x.iter() + .fold( + (Vec::new(), SpringStatus::Working, 0_u32), + |(mut array, _last, mut current_run), x| { + if *x == SpringStatus::Failing { + current_run += 1; + } else { + if current_run > 0 { + array.push(current_run); + } + current_run = 0; + } + (array, *x, current_run) + }, + ); + if last == SpringStatus::Failing { + array.push(current_run); + } + array + .iter() + .zip(self.broken_spans.iter()) + .all(|(a, b)| a == b) + }) + .count() + } + + //fn generate_permiatation(&self) +} + +/// day 12 part 1 of aoc 2023 +/// +/// # Arguments +/// - input the input for today's puzzle +/// +/// # Panics +/// panics whne it cannot parse the input OR when ever the number of game numbers is greater than +/// usize #[must_use] -pub fn part1 (_input: &str) -> String { - "Not Finished".to_string() +pub fn part1(input: &str) -> String { + let (_, spas) = parse_input(input).expect("AOC always has valid input"); + spas.iter() + .map(|x| x.process() as u64) + .sum::() + .to_string() +} + +fn parse_spa_spots(input: &str) -> IResult<&str, Vec> { + many1(alt(( + tag(".").map(|_| SpringStatus::Working), + tag("#").map(|_| SpringStatus::Failing), + tag("?").map(|_| SpringStatus::Unknown), + )))(input) +} + +fn parse_spa_spans(input: &str) -> IResult<&str, Vec> { + separated_list1(tag(","), complete::u32)(input) +} + +fn parse_spa_rows(input: &str) -> IResult<&str, Row> { + separated_pair(parse_spa_spots, complete::space1, parse_spa_spans)(input).map( + |(input, (springs, broken_spans))| { + ( + input, + Row { + springs, + broken_spans, + }, + ) + }, + ) +} + +fn parse_input(input: &str) -> IResult<&str, Vec> { + separated_list1(complete::line_ending, parse_spa_rows)(input) } #[cfg(test)] mod test { use super::*; + use rstest::rstest; - const INPUT: &str = ""; + #[rstest] + #[case("???.### 1,1,3", 1)] + #[case(".??..??...?##. 1,1,3", 4)] + #[case("?#?#?#?#?#?#?#? 1,3,1,6", 1)] + #[case("????.#...#... 4,1,1", 1)] + #[case("????.######..#####. 1,6,5", 4)] + #[case("?###???????? 3,2,1", 10)] + fn line_test(#[case] input: &str, #[case] expected: usize) { + let (_, row) = parse_spa_rows(input).expect("should parse"); + assert_eq!(row.process(), expected); + } + + const INPUT: &str = "???.### 1,1,3 +.??..??...?##. 1,1,3 +?#?#?#?#?#?#?#? 1,3,1,6 +????.#...#... 4,1,1 +????.######..#####. 1,6,5 +?###???????? 3,2,1"; #[test] fn part1_works() { let result = part1(INPUT); - assert_eq!(result, "Not Finished".to_string()); + assert_eq!(result, "21".to_string()); } } diff --git a/2023/day-12/src/part2.rs b/2023/day-12/src/part2.rs index 8f15571..7e28c2f 100644 --- a/2023/day-12/src/part2.rs +++ b/2023/day-12/src/part2.rs @@ -1,20 +1,171 @@ #![warn(clippy::all, clippy::pedantic)] +use std::{iter::repeat, collections::HashMap}; + +use itertools::Itertools; +use nom::{ + bytes::complete::{tag, is_a}, + character::complete, + multi::separated_list1, + sequence::separated_pair, + IResult}; + + +struct Row { + springs: String, + broken_spans: Vec, +} + +impl Row { + fn process(&self) -> usize { + let mut cache = HashMap::new(); + self.dynamic_search(&mut cache, (0,0,0)) + } + + fn dynamic_search(&self, cache: &mut HashMap<(usize,usize,u32),usize>, search: (usize, usize, u32)) -> usize { + let (data_index, group_index, group_size) = search; + //are we at the end of the input + if data_index >= self.springs.len(){ + // when group_index is greater we are here then golden + if group_index >= self.broken_spans.len() { + return 1; + } + + //we haven't satisfied groups but the end is failing + if group_index == self.broken_spans.len() - 1 && self.broken_spans[group_index] == group_size { + return 1; + } + return 0; + } + match self.springs.as_bytes()[data_index] { + b'.' => { + //previous was also working just go to next data point + if group_size == 0 { + return self.dynamic_search(cache, (data_index + 1, group_index, 0)); + } + + //we failed to match the group + if group_index >= self.broken_spans.len() || self.broken_spans[group_index] != group_size{ + return 0; + } + + //completed a group keep going + self.dynamic_search(cache,(data_index+1, group_index +1, 0)) + }, + b'#' => { + //too many for our group + if group_index >= self.broken_spans.len() || group_size + 1 > self.broken_spans[group_index] { + return 0; + } + + //haven't completed group yet keep looking + self.dynamic_search(cache, (data_index+1, group_index, group_size + 1)) + }, + b'?' => { + if let Some(res) = cache.get(&(data_index,group_index,group_size)).copied() { + return res; + } + + let mut perms = 0_usize; + + //pretend to be a undamaged, if in a working group + if 0 == group_size { + perms += self.dynamic_search(cache, (data_index +1, group_index, 0)); + } + + //pretend to be damaged + if group_index < self.broken_spans.len() && group_size < self.broken_spans[group_index] { + perms += self.dynamic_search(cache, (data_index + 1, group_index, group_size +1)); + } + + //pretend to be undamage, thus ending a damaged group + if group_index < self.broken_spans.len() && group_size == self.broken_spans[group_index] { + perms += self.dynamic_search(cache, (data_index +1, group_index+1, 0)); + } + + cache.insert((data_index, group_index, group_size),perms); + perms + }, + _ => unreachable!(), + } + } +} + +/// day 12 part 2 of aoc 2023 +/// +/// # Arguments +/// - input the input for today's puzzle +/// +/// # Panics +/// panics whne it cannot parse the input OR when ever the number of game numbers is greater than +/// usize #[must_use] -pub fn part2 (_input: &str) -> String { - "Not Finished".to_string() +pub fn part2(input: &str) -> String { + let (_, spas) = parse_input(input).expect("AOC always has valid input"); + spas.iter() + .map(|x| x.process() as u64) + .sum::() + .to_string() +} + +fn parse_spa_spots(input: &str) -> IResult<&str, &str> { + is_a(".#?")(input) +} + +fn parse_spa_spans(input: &str) -> IResult<&str, Vec> { + separated_list1(tag(","), complete::u32)(input) +} + +fn parse_spa_rows(input: &str) -> IResult<&str, Row> { + separated_pair(parse_spa_spots, complete::space1, parse_spa_spans)(input).map( + |(input, (springs, broken_spans))| { + ( + input, + Row { + springs: std::iter::repeat(springs).take(5).join("?"), + broken_spans: repeat(broken_spans.iter()) + .take(5) + .flatten() + .copied() + .collect(), + }, + ) + }, + ) +} + +fn parse_input(input: &str) -> IResult<&str, Vec> { + separated_list1(complete::line_ending, parse_spa_rows)(input) } #[cfg(test)] mod test { use super::*; + use rstest::rstest; - const INPUT: &str = ""; + #[rstest] + #[case("???.### 1,1,3", 1)] + #[case(".??..??...?##. 1,1,3", 16_384)] + #[case("?#?#?#?#?#?#?#? 1,3,1,6", 1)] + #[case("????.#...#... 4,1,1", 16)] + #[case("????.######..#####. 1,6,5", 2_500)] + #[case("?###???????? 3,2,1", 506_250)] + fn line_test(#[case] input: &str, #[case] expected: usize) { + let (_, row) = parse_spa_rows(input).expect("should parse"); + assert_eq!(row.process(), expected); + } + + const INPUT: &str = "???.### 1,1,3 +.??..??...?##. 1,1,3 +?#?#?#?#?#?#?#? 1,3,1,6 +????.#...#... 4,1,1 +????.######..#####. 1,6,5 +?###???????? 3,2,1"; #[test] fn part2_works() { let result = part2(INPUT); - assert_eq!(result, "Not Finished".to_string()); + assert_eq!(result, "525152".to_string()); } }