diff --git a/README.md b/README.md index 11f87b8..0f71b46 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. | [Day 2](./src/bin/02.rs) | `1.1ms` | `3.8ms` | | [Day 3](./src/bin/03.rs) | `85.1µs` | `121.6µs` | | [Day 4](./src/bin/04.rs) | `244.2µs` | `4.1ms` | +| [Day 5](./src/bin/05.rs) | `196.7µs` | `66.3µs` | -**Total: 9.68ms** +**Total: 9.94ms** --- diff --git a/data/examples/05.txt b/data/examples/05.txt new file mode 100644 index 0000000..32fe3d3 --- /dev/null +++ b/data/examples/05.txt @@ -0,0 +1,12 @@ +3-5 +10-14 +16-20 +12-18 + +1 +5 +8 +11 +17 +32 + diff --git a/src/bin/05.rs b/src/bin/05.rs new file mode 100644 index 0000000..15a885e --- /dev/null +++ b/src/bin/05.rs @@ -0,0 +1,110 @@ +advent_of_code::solution!(5); + +/// A range of fresh ingredient IDs (inclusive) +struct FreshRange { + start: u64, + end: u64, +} + +impl FreshRange { + fn contains(&self, id: u64) -> bool { + id >= self.start && id <= self.end + } +} + +/// Parse the input into fresh ranges and available IDs +fn parse_input(input: &str) -> (Vec, Vec) { + // Normalize line endings and split on blank line + let normalized = input.replace("\r\n", "\n"); + let mut sections = normalized.split("\n\n"); + + // Parse fresh ingredient ID ranges + let ranges_section = sections.next().unwrap_or(""); + let ranges: Vec = ranges_section + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| { + let mut parts = line.split('-'); + let start: u64 = parts.next().unwrap().parse().unwrap(); + let end: u64 = parts.next().unwrap().parse().unwrap(); + FreshRange { start, end } + }) + .collect(); + + // Parse available ingredient IDs + let ids_section = sections.next().unwrap_or(""); + let ids: Vec = ids_section + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.trim().parse().unwrap()) + .collect(); + + (ranges, ids) +} + +/// Check if an ingredient ID is fresh (falls within any range) +fn is_fresh(id: u64, ranges: &[FreshRange]) -> bool { + ranges.iter().any(|range| range.contains(id)) +} + +pub fn part_one(input: &str) -> Option { + let (ranges, ids) = parse_input(input); + + let fresh_count = ids.iter().filter(|&&id| is_fresh(id, &ranges)).count(); + + Some(fresh_count as u64) +} + +/// Merge overlapping ranges and count total unique IDs +fn count_unique_fresh_ids(mut ranges: Vec) -> u64 { + if ranges.is_empty() { + return 0; + } + + // Sort ranges by start + ranges.sort_by_key(|r| r.start); + + // Merge overlapping ranges + let mut merged: Vec = Vec::new(); + merged.push(FreshRange { + start: ranges[0].start, + end: ranges[0].end, + }); + + for range in ranges.into_iter().skip(1) { + let last = merged.last_mut().unwrap(); + // Check if ranges overlap or are adjacent + if range.start <= last.end + 1 { + // Extend the current range if needed + last.end = last.end.max(range.end); + } else { + // No overlap, start a new range + merged.push(range); + } + } + + // Count total unique IDs across all merged ranges + merged.iter().map(|r| r.end - r.start + 1).sum() +} + +pub fn part_two(input: &str) -> Option { + let (ranges, _) = parse_input(input); + Some(count_unique_fresh_ids(ranges)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + let result = part_one(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(3)); + } + + #[test] + fn test_part_two() { + let result = part_two(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(14)); + } +}