This document explains a Rust program that validates sequences of numbers and counts valid patterns according to specific criteria. The program examines "reports" containing numeric sequences, validates them against rules, and then explores how removing elements affects validity.
The core problem involves validating sequences of numbers according to specific rules:
- Each sequence must move consistently in one direction (increasing or decreasing)
- Consecutive numbers must differ by 1, 2, or 3 units
- Part 2 explores whether removing any single element can make an invalid sequence valid
This is essentially a pattern recognition problem where we need to:
- Identify valid sequences according to strict rules
- Count sequences that are already valid
- Count sequences that can become valid by removing one element
We represent each numeric sequence as a Report
struct with the following structure:
#[derive(Debug)]
struct Report {
levels: rc::Rc<[usize]>,
}
The Rc<[usize]>
(reference-counted array) is chosen to efficiently handle the sequences with shared ownership. This avoids unnecessary copying of data when manipulating sequences.
The program parses the input file line by line, converting each line into a Report
:
fn main() {
let input = fs::read_to_string("src/bin/day2/input.txt").expect("File not found");
let lists = input
.lines()
.map(|line| line.parse::<Report>().expect("Invalid list"))
.collect::<Vec<Report>>();
// ...
}
We implement the FromStr
trait to allow parsing strings directly into our Report
struct:
impl FromStr for Report {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Report {
levels: s
.split_ascii_whitespace()
.map(|n| n.parse::<usize>())
.collect::<Result<rc::Rc<[usize]>, ParseIntError>>()?
})
}
}
This approach leverages Rust's trait system to make the code more readable and maintainable, separating the parsing logic from the main program flow.
The core algorithm checks two conditions for each sequence:
fn validate(r: &[usize]) -> bool {
let dir = r[0] < r[1];
r.windows(2)
.all(|a| {
(1..=3).contains(&(a[0].abs_diff(a[1])))
&& match dir {
true => a[0] < a[1],
false => a[0] > a[1],
}
})
}
This function:
- Determines the direction (increasing or decreasing) based on the first two numbers
- Uses the
windows(2)
method to check consecutive pairs of elements - Verifies two conditions for each pair:
- The absolute difference is between 1 and 3 (inclusive)
- The direction remains consistent throughout the sequence
The all()
combinator ensures that every pair satisfies both conditions.
For part 1, we simply count the sequences that are already valid:
let count = lists.iter().filter(|r| r.is_safe()).count();
The is_safe()
method is a simple wrapper around our validation function:
fn is_safe(&self) -> bool {
Report::validate(&self.levels)
}
In part 2, we need to determine if removing any single element makes an invalid sequence valid:
fn is_safe_dumpen(&self) -> bool {
(0..self.levels.len()).any(|p| {
let mut levels = self.levels.to_vec();
levels.remove(p);
Report::validate(&levels)
})
}
This function:
- Iterates through each position in the sequence
- Creates a modified copy with that element removed
- Checks if the modified sequence is valid
- Returns true if any modified sequence is valid
We then count the sequences that satisfy this condition:
let count = lists.iter().filter(|r| r.is_safe_dumpen()).count();
The program includes timing code to measure performance:
let t = time::Instant::now();
// code to time
println!("Part 1: {} = {:?}", count, t.elapsed());
Several design choices improve efficiency:
- Using
Rc<[usize]>
allows efficient cloning of sequences - The
windows(2)
approach avoids manual indexing and bounds checking - The
any()
combinator inis_safe_dumpen()
short-circuits once a valid modification is found
This program demonstrates effective use of Rust's features:
- Strong typing and error handling with the
Result
type - Trait implementations for clean parsing
- Functional programming with iterators and combinators
- Efficient ownership management with
Rc
The algorithm itself highlights the importance of breaking down complex validation into simpler rules and leveraging Rust's standard library to express those rules concisely and efficiently.