Refactor days 1-2, optimize day 3 part 1 with suffix max

This commit is contained in:
Russell Champoux committed 2025-12-04 17:34:11 -05:00
1 parent 0b5c8fa10f
commit d650dce61b
6 files changed
+109 -125

No files matched your search

Generated
+16
View File
@@ -23,6 +23,7 @@ version = "0.12.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"dhat", "dhat",
"itertools",
"pico-args", "pico-args",
"tinyjson", "tinyjson",
] ]
@@ -126,6 +127,12 @@ dependencies = [
"thousands", "thousands",
] ]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.1" version = "0.28.1"
@@ -155,6 +162,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.9"
+1
View File
@@ -24,6 +24,7 @@ test_lib = []
# Template dependencies # Template dependencies
chrono = { version = "0.4.38", optional = true } chrono = { version = "0.4.38", optional = true }
dhat = { version = "0.3.3", optional = true } dhat = { version = "0.3.3", optional = true }
itertools = "0.14.0"
pico-args = "0.5.0" pico-args = "0.5.0"
tinyjson = "2.5.1" tinyjson = "2.5.1"
+4 -4
View File
@@ -11,11 +11,11 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www.
| Day | Part 1 | Part 2 | | Day | Part 1 | Part 2 |
| :---: | :---: | :---: | | :---: | :---: | :---: |
| [Day 1](./src/bin/01.rs) | `113.0µs` | `119.9µs` | | [Day 1](./src/bin/01.rs) | `102.5µs` | `123.5µs` |
| [Day 2](./src/bin/02.rs) | `54.7ms` | `71.6ms` | | [Day 2](./src/bin/02.rs) | `45.0ms` | `69.6ms` |
| [Day 3](./src/bin/03.rs) | `802.9µs` | `131.1µs` | | [Day 3](./src/bin/03.rs) | `85.1µs` | `121.6µs` |
**Total: 127.47ms** **Total: 115.03ms**
<!--- benchmarking table ---> <!--- benchmarking table --->
--- ---
+43 -50
View File
@@ -1,50 +1,29 @@
advent_of_code::solution!(1); advent_of_code::solution!(1);
pub fn part_one(input: &str) -> Option<u64> { /// Parse a rotation instruction like "L68" or "R14" into (direction, distance)
let mut position: i32 = 50; fn parse_rotation(line: &str) -> Option<(char, i32)> {
let mut count = 0u64;
for line in input.lines() {
let line = line.trim(); let line = line.trim();
if line.is_empty() { if line.is_empty() {
continue; return None;
}
let direction = line.chars().next()?;
let distance: i32 = line[1..].parse().ok()?;
Some((direction, distance))
} }
let direction = &line[0..1]; /// Apply a rotation and return the new position
let distance: i32 = line[1..].parse().ok()?; fn apply_rotation(position: i32, direction: char, distance: i32) -> i32 {
match direction { match direction {
"L" => position = (position - distance).rem_euclid(100), 'L' => (position - distance).rem_euclid(100),
"R" => position = (position + distance).rem_euclid(100), 'R' => (position + distance).rem_euclid(100),
_ => return None, _ => position,
}
if position == 0 {
count += 1;
} }
} }
Some(count) /// Count how many times we pass through 0 during a rotation
} fn count_zeros(position: i32, direction: char, distance: i32) -> i32 {
match direction {
pub fn part_two(input: &str) -> Option<u64> { 'L' => {
let mut position: i32 = 50;
let mut count = 0u64;
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let direction = &line[0..1];
let distance: i32 = line[1..].parse().ok()?;
// Count how many times we pass through 0 during this rotation
let zeros = match direction {
"L" => {
// Moving left: we hit 0 at clicks p, p+100, p+200, ... (if p > 0)
// or at clicks 100, 200, 300, ... (if p = 0)
if position == 0 { if position == 0 {
distance / 100 distance / 100
} else if distance >= position { } else if distance >= position {
@@ -53,9 +32,7 @@ pub fn part_two(input: &str) -> Option<u64> {
0 0
} }
} }
"R" => { 'R' => {
// Moving right: we hit 0 at click (100-p), then (100-p)+100, etc.
// or at clicks 100, 200, 300, ... (if p = 0)
if position == 0 { if position == 0 {
distance / 100 distance / 100
} else if distance >= 100 - position { } else if distance >= 100 - position {
@@ -64,19 +41,35 @@ pub fn part_two(input: &str) -> Option<u64> {
0 0
} }
} }
_ => return None, _ => 0,
};
count += zeros as u64;
// Update position
match direction {
"L" => position = (position - distance).rem_euclid(100),
"R" => position = (position + distance).rem_euclid(100),
_ => {}
} }
} }
pub fn part_one(input: &str) -> Option<u64> {
let count = input
.lines()
.filter_map(parse_rotation)
.fold((50i32, 0u64), |(pos, count), (dir, dist)| {
let new_pos = apply_rotation(pos, dir, dist);
let new_count = count + if new_pos == 0 { 1 } else { 0 };
(new_pos, new_count)
})
.1;
Some(count)
}
pub fn part_two(input: &str) -> Option<u64> {
let count = input
.lines()
.filter_map(parse_rotation)
.fold((50i32, 0u64), |(pos, count), (dir, dist)| {
let zeros = count_zeros(pos, dir, dist) as u64;
let new_pos = apply_rotation(pos, dir, dist);
(new_pos, count + zeros)
})
.1;
Some(count) Some(count)
} }
+33 -66
View File
@@ -1,41 +1,22 @@
use itertools::Itertools;
advent_of_code::solution!(2); advent_of_code::solution!(2);
/// Parse a range string like "11-22" into (start, end)
fn parse_range(s: &str) -> Option<(u64, u64)> {
s.trim()
.split('-')
.map(|x| x.parse::<u64>().ok())
.collect_tuple()
.and_then(|(a, b)| Some((a?, b?)))
}
/// Check if a number is "invalid" - made of a pattern repeated exactly twice /// Check if a number is "invalid" - made of a pattern repeated exactly twice
fn is_invalid_part1(n: u64) -> bool { fn is_invalid_part1(n: u64) -> bool {
let s = n.to_string(); let s = n.to_string();
let len = s.len(); let len = s.len();
// Must have even length for pattern to repeat exactly twice // Must have even length for pattern to repeat exactly twice
if len % 2 != 0 { len % 2 == 0 && s[..len / 2] == s[len / 2..]
return false;
}
let half = len / 2;
s[..half] == s[half..]
}
pub fn part_one(input: &str) -> Option<u64> {
let mut sum = 0u64;
// Parse comma-separated ranges, handling potential whitespace/newlines
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
// Parse "start-end" range
let mut range_parts = part.split('-');
let start: u64 = range_parts.next()?.parse().ok()?;
let end: u64 = range_parts.next()?.parse().ok()?;
// Check each number in the range
for n in start..=end {
if is_invalid_part1(n) {
sum += n;
}
}
}
Some(sum)
} }
/// Check if a number is "invalid" - made of a pattern repeated at least twice /// Check if a number is "invalid" - made of a pattern repeated at least twice
@@ -44,50 +25,36 @@ fn is_invalid_part2(n: u64) -> bool {
let len = s.len(); let len = s.len();
// Try each possible pattern length (1 to len/2) // Try each possible pattern length (1 to len/2)
for pattern_len in 1..=len / 2 { (1..=len / 2).any(|pattern_len| {
// Pattern can only work if it divides evenly // Pattern can only work if it divides evenly
if len % pattern_len != 0 { len % pattern_len == 0 && {
continue;
}
let pattern = &s[..pattern_len]; let pattern = &s[..pattern_len];
let mut is_match = true; // Check if all chunks match the pattern
s.as_bytes()
// Check if the entire string is made of this pattern repeated .chunks(pattern_len)
for i in (pattern_len..len).step_by(pattern_len) { .all(|chunk| chunk == pattern.as_bytes())
if &s[i..i + pattern_len] != pattern {
is_match = false;
break;
} }
})
} }
if is_match { pub fn part_one(input: &str) -> Option<u64> {
return true; let sum = input
} .split(',')
} .filter_map(|part| parse_range(part))
.flat_map(|(start, end)| start..=end)
.filter(|&n| is_invalid_part1(n))
.sum();
false Some(sum)
} }
pub fn part_two(input: &str) -> Option<u64> { pub fn part_two(input: &str) -> Option<u64> {
let mut sum = 0u64; let sum = input
.split(',')
for part in input.split(',') { .filter_map(|part| parse_range(part))
let part = part.trim(); .flat_map(|(start, end)| start..=end)
if part.is_empty() { .filter(|&n| is_invalid_part2(n))
continue; .sum();
}
let mut range_parts = part.split('-');
let start: u64 = range_parts.next()?.parse().ok()?;
let end: u64 = range_parts.next()?.parse().ok()?;
for n in start..=end {
if is_invalid_part2(n) {
sum += n;
}
}
}
Some(sum) Some(sum)
} }
+12 -5
View File
@@ -2,13 +2,20 @@ advent_of_code::solution!(3);
/// Find the maximum 2-digit number by selecting exactly 2 batteries from a bank /// Find the maximum 2-digit number by selecting exactly 2 batteries from a bank
fn max_joltage_2(bank: &[u8]) -> u64 { fn max_joltage_2(bank: &[u8]) -> u64 {
let mut max = 0u64; let n = bank.len();
// For each position as the tens digit // Precompute suffix maximums: suffix_max[i] = max digit from position i to end
for i in 0..bank.len() - 1 { // This avoids O(n) scan for each position, making overall O(n) instead of O(n²)
let mut suffix_max = vec![0u8; n];
suffix_max[n - 1] = bank[n - 1];
for i in (0..n - 1).rev() {
suffix_max[i] = bank[i].max(suffix_max[i + 1]);
}
let mut max = 0u64;
for i in 0..n - 1 {
let tens = bank[i] as u64; let tens = bank[i] as u64;
// Find max digit after position i let units = suffix_max[i + 1] as u64; // O(1) lookup instead of O(n) scan
let units = *bank[i + 1..].iter().max().unwrap() as u64;
let joltage = tens * 10 + units; let joltage = tens * 10 + units;
max = max.max(joltage); max = max.max(joltage);
} }