diff options
Diffstat (limited to '2023/src/bin/day_12.rs')
-rw-r--r-- | 2023/src/bin/day_12.rs | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/2023/src/bin/day_12.rs b/2023/src/bin/day_12.rs new file mode 100644 index 0000000..e60b450 --- /dev/null +++ b/2023/src/bin/day_12.rs @@ -0,0 +1,323 @@ +use nom::{ + branch::alt, + character::complete::{char, line_ending, space1, u32}, + combinator::{map, value}, + multi::{many1, many1_count, separated_list1}, + sequence::separated_pair, + IResult, +}; +use std::{collections::BTreeMap, fs}; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let input = fs::read_to_string("inputs/day_12.txt")?; + let small = SpringField::parser(&input).unwrap().1; + dbg!(&small.possibilities_sum()); + + let large = small.expand(); + dbg!(&large.possibilities_sum()); + + Ok(()) +} + +#[derive(Debug)] +struct SpringField(Vec<SpringRow>); + +#[derive(Debug, Clone)] +struct SpringRow { + springs: Vec<Spring>, + check: Vec<u32>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Spring { + Good, + Bad(usize), + Unknown, +} + +impl SpringField { + fn parser(input: &str) -> IResult<&str, Self> { + map(separated_list1(line_ending, SpringRow::parser), SpringField)(input) + } + + fn possibilities_sum(&self) -> usize { + self.0.iter().map(|r| r.possibilities_count()).sum() + } + + fn expand(&self) -> SpringField { + SpringField(self.0.iter().map(|r| r.expand()).collect()) + } +} + +impl SpringRow { + fn parser(input: &str) -> IResult<&str, Self> { + map( + separated_pair( + many1(Spring::parser), + space1, + separated_list1(char(','), u32), + ), + |(springs, check)| SpringRow { springs, check }, + )(input) + } + + fn expand(&self) -> SpringRow { + let mut expanded = SpringRow { + springs: Vec::new(), + check: Vec::new(), + }; + + for _ in 0..5 { + expanded.springs.append(&mut self.springs.clone()); + expanded.springs.push(Spring::Unknown); + expanded.check.append(&mut self.check.clone()); + } + + // should only have the extra Unknown between, not at the very end. + expanded.springs.pop(); + + expanded + } + + fn possibilities_count(&self) -> usize { + let optimized = self.optimize_tail(); + optimized.possibilities_count_inner(0, 0, &mut BTreeMap::new()) + } + + fn optimize_tail(&self) -> SpringRow { + let mut optimized = self.clone(); + while optimized.springs.len() > 0 && optimized.check.len() > 0 { + match optimized.springs[optimized.springs.len() - 1] { + Spring::Good => { + optimized.springs.pop(); + } + Spring::Bad(s) if s > optimized.check[optimized.check.len() - 1] as usize => { + panic!("Unsolvable row"); + } + Spring::Bad(s) if s <= optimized.check[optimized.check.len() - 1] as usize => { + optimized.trim_bad_suffix(); + } + Spring::Bad(_) => unreachable!(), + Spring::Unknown => { + break; + } + } + } + + optimized + } + + fn trim_bad_suffix(&mut self) { + let last_check = self.check.pop().expect("No trailing check to pop") as usize; + let mut check_i = 0; + while check_i < last_check { + match self + .springs + .pop() + .expect("Ran out of springs while optimizing suffix!") + { + Spring::Unknown => { + check_i += 1; + } + Spring::Bad(inc) => { + check_i += inc; + } + Spring::Good => { + panic!("Found a good spring in the middle of what must be a bad spring range"); + } + } + } + let is_boundary = self.springs.len() == 0; + let has_extra_good_or_unknown_to_trim = + !is_boundary && !matches!(self.springs.last(), Some(Spring::Bad(_))); + + let valid_suffix = + check_i == last_check && (is_boundary || has_extra_good_or_unknown_to_trim); + if !valid_suffix { + panic!("The suffix had another invalid bad section immediately!"); + } + + if has_extra_good_or_unknown_to_trim { + self.springs.pop(); + } + } + + fn possibilities_count_inner( + &self, + springs_offset: usize, + check_offset: usize, + cache: &mut BTreeMap<(usize, usize), usize>, + ) -> usize { + if let Some(cached) = cache.get(&(springs_offset, check_offset)) { + return *cached; + }; + + let (springs_offset, check_offset) = match self.optimize_head(springs_offset, check_offset) + { + Some(optimized) => optimized, + None => { + return 0; + } + }; + + let springs = &self.springs[springs_offset..]; + let check = &self.check[check_offset..]; + + if check.len() == 1 && springs.iter().all(|s| !matches!(s, Spring::Bad(_))) { + let mut contigous_unknowns = Vec::new(); + let mut next_contiguous_unknown = 0; + let mut is_in_unknown = false; + for spring in springs.iter() { + match spring { + Spring::Unknown => { + next_contiguous_unknown += 1; + is_in_unknown = true; + } + Spring::Good => { + if is_in_unknown { + contigous_unknowns.push(next_contiguous_unknown); + next_contiguous_unknown = 0; + is_in_unknown = false; + } + } + Spring::Bad(_) => unreachable!(), + } + } + if is_in_unknown { + contigous_unknowns.push(next_contiguous_unknown); + } + + return contigous_unknowns + .iter() + .map(|region| { + if *region >= check[0] as usize { + region - check[0] as usize + 1 + } else { + 0 + } + }) + .sum(); + } + + if check.len() == 0 { + if springs.iter().any(|s| matches!(s, Spring::Bad(_))) { + return 0; + } else { + return 1; + } + } + + let valid_prefix_possibilities_count = + if let Some((without_prefix_springs_offset, without_prefix_check_offset)) = + self.trim_bad_prefix(springs_offset, check_offset) + { + self.possibilities_count_inner( + without_prefix_springs_offset, + without_prefix_check_offset, + cache, + ) + } else { + 0 + }; + + let non_prefix_possibilities_count = + self.possibilities_count_inner(springs_offset + 1, check_offset, cache); + + let total_possibilities = valid_prefix_possibilities_count + non_prefix_possibilities_count; + + cache.insert((springs_offset, check_offset), total_possibilities); + + total_possibilities + } + + fn optimize_head( + &self, + mut springs_offset: usize, + mut check_offset: usize, + ) -> Option<(usize, usize)> { + while springs_offset < self.springs.len() && check_offset < self.check.len() { + match self.springs[springs_offset] { + Spring::Good => { + springs_offset += 1; + } + Spring::Bad(s) if s > self.check[check_offset] as usize => { + return None; + } + Spring::Bad(s) if s <= self.check[check_offset] as usize => { + match self.trim_bad_prefix(springs_offset, check_offset) { + Some((new_springs, new_check)) => { + springs_offset = new_springs; + check_offset = new_check; + } + None => return None, + }; + } + Spring::Bad(_) => unreachable!(), + Spring::Unknown => { + break; + } + } + } + + if springs_offset == self.springs.len() && check_offset != self.check.len() { + None + } else { + Some((springs_offset, check_offset)) + } + } + + fn trim_bad_prefix( + &self, + springs_offset: usize, + check_offset: usize, + ) -> Option<(usize, usize)> { + let first_check = self.check[check_offset] as usize; + let mut check_i = 0; + let mut springs_i = springs_offset; + while check_i < first_check { + if springs_i >= self.springs.len() { + return None; + } + + match self.springs[springs_i] { + Spring::Unknown => { + check_i += 1; + } + Spring::Bad(inc) => { + check_i += inc; + } + Spring::Good => { + return None; + } + } + springs_i += 1; + } + + let is_boundary = springs_i == self.springs.len(); + let has_extra_good_or_unknown_to_trim = + !is_boundary && !matches!(self.springs[springs_i], Spring::Bad(_)); + let valid_prefix = + check_i == first_check && (is_boundary || has_extra_good_or_unknown_to_trim); + + if valid_prefix { + let new_springs_i = if has_extra_good_or_unknown_to_trim { + springs_i + 1 + } else { + springs_i + }; + Some((new_springs_i, check_offset + 1)) + } else { + None + } + } +} + +impl Spring { + fn parser(input: &str) -> IResult<&str, Self> { + alt(( + value(Spring::Good, many1_count(char('.'))), + map(many1_count(char('#')), Spring::Bad), + value(Spring::Unknown, char('?')), + ))(input) + } +} |