From d650dce61be88940463d7a30b40abbed2ad7fa71 Mon Sep 17 00:00:00 2001 From: Russell Champoux Date: Thu, 4 Dec 2025 17:34:11 -0500 Subject: [PATCH] Refactor days 1-2, optimize day 3 part 1 with suffix max --- Cargo.lock | 16 ++++++ Cargo.toml | 1 + README.md | 8 +-- src/bin/01.rs | 133 ++++++++++++++++++++++++-------------------------- src/bin/02.rs | 103 +++++++++++++------------------------- src/bin/03.rs | 21 +++++--- 6 files changed, 133 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8147679..355c537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ version = "0.12.0" dependencies = [ "chrono", "dhat", + "itertools", "pico-args", "tinyjson", ] @@ -126,6 +127,12 @@ dependencies = [ "thousands", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "gimli" version = "0.28.1" @@ -155,6 +162,15 @@ dependencies = [ "cc", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" diff --git a/Cargo.toml b/Cargo.toml index b1943db..27532cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ test_lib = [] # Template dependencies chrono = { version = "0.4.38", optional = true } dhat = { version = "0.3.3", optional = true } +itertools = "0.14.0" pico-args = "0.5.0" tinyjson = "2.5.1" diff --git a/README.md b/README.md index f5bb334..ed5d562 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. | Day | Part 1 | Part 2 | | :---: | :---: | :---: | -| [Day 1](./src/bin/01.rs) | `113.0µs` | `119.9µs` | -| [Day 2](./src/bin/02.rs) | `54.7ms` | `71.6ms` | -| [Day 3](./src/bin/03.rs) | `802.9µs` | `131.1µs` | +| [Day 1](./src/bin/01.rs) | `102.5µs` | `123.5µs` | +| [Day 2](./src/bin/02.rs) | `45.0ms` | `69.6ms` | +| [Day 3](./src/bin/03.rs) | `85.1µs` | `121.6µs` | -**Total: 127.47ms** +**Total: 115.03ms** --- diff --git a/src/bin/01.rs b/src/bin/01.rs index 624c0ae..2a7d9ee 100644 --- a/src/bin/01.rs +++ b/src/bin/01.rs @@ -1,81 +1,74 @@ advent_of_code::solution!(1); -pub fn part_one(input: &str) -> Option { - 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()?; - - match direction { - "L" => position = (position - distance).rem_euclid(100), - "R" => position = (position + distance).rem_euclid(100), - _ => return None, - } - - if position == 0 { - count += 1; - } +/// Parse a rotation instruction like "L68" or "R14" into (direction, distance) +fn parse_rotation(line: &str) -> Option<(char, i32)> { + let line = line.trim(); + if line.is_empty() { + return None; } + let direction = line.chars().next()?; + let distance: i32 = line[1..].parse().ok()?; + Some((direction, distance)) +} + +/// Apply a rotation and return the new position +fn apply_rotation(position: i32, direction: char, distance: i32) -> i32 { + match direction { + 'L' => (position - distance).rem_euclid(100), + 'R' => (position + distance).rem_euclid(100), + _ => position, + } +} + +/// Count how many times we pass through 0 during a rotation +fn count_zeros(position: i32, direction: char, distance: i32) -> i32 { + match direction { + 'L' => { + if position == 0 { + distance / 100 + } else if distance >= position { + (distance - position) / 100 + 1 + } else { + 0 + } + } + 'R' => { + if position == 0 { + distance / 100 + } else if distance >= 100 - position { + (distance + position - 100) / 100 + 1 + } else { + 0 + } + } + _ => 0, + } +} + +pub fn part_one(input: &str) -> Option { + 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 { - 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 { - distance / 100 - } else if distance >= position { - (distance - position) / 100 + 1 - } else { - 0 - } - } - "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 { - distance / 100 - } else if distance >= 100 - position { - (distance + position - 100) / 100 + 1 - } else { - 0 - } - } - _ => return None, - }; - - count += zeros as u64; - - // Update position - match direction { - "L" => position = (position - distance).rem_euclid(100), - "R" => position = (position + distance).rem_euclid(100), - _ => {} - } - } + 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) } diff --git a/src/bin/02.rs b/src/bin/02.rs index 8d1722c..87768fd 100644 --- a/src/bin/02.rs +++ b/src/bin/02.rs @@ -1,41 +1,22 @@ +use itertools::Itertools; + 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::().ok()) + .collect_tuple() + .and_then(|(a, b)| Some((a?, b?))) +} + /// Check if a number is "invalid" - made of a pattern repeated exactly twice fn is_invalid_part1(n: u64) -> bool { let s = n.to_string(); let len = s.len(); // Must have even length for pattern to repeat exactly twice - if len % 2 != 0 { - return false; - } - let half = len / 2; - s[..half] == s[half..] -} - -pub fn part_one(input: &str) -> Option { - 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) + len % 2 == 0 && s[..len / 2] == s[len / 2..] } /// 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(); // 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 - if len % pattern_len != 0 { - continue; + len % pattern_len == 0 && { + let pattern = &s[..pattern_len]; + // Check if all chunks match the pattern + s.as_bytes() + .chunks(pattern_len) + .all(|chunk| chunk == pattern.as_bytes()) } + }) +} - let pattern = &s[..pattern_len]; - let mut is_match = true; +pub fn part_one(input: &str) -> Option { + let sum = input + .split(',') + .filter_map(|part| parse_range(part)) + .flat_map(|(start, end)| start..=end) + .filter(|&n| is_invalid_part1(n)) + .sum(); - // Check if the entire string is made of this pattern repeated - for i in (pattern_len..len).step_by(pattern_len) { - if &s[i..i + pattern_len] != pattern { - is_match = false; - break; - } - } - - if is_match { - return true; - } - } - - false + Some(sum) } pub fn part_two(input: &str) -> Option { - let mut sum = 0u64; - - for part in input.split(',') { - let part = part.trim(); - if part.is_empty() { - continue; - } - - 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; - } - } - } + let sum = input + .split(',') + .filter_map(|part| parse_range(part)) + .flat_map(|(start, end)| start..=end) + .filter(|&n| is_invalid_part2(n)) + .sum(); Some(sum) } diff --git a/src/bin/03.rs b/src/bin/03.rs index a8a2994..86e5a06 100644 --- a/src/bin/03.rs +++ b/src/bin/03.rs @@ -2,13 +2,20 @@ advent_of_code::solution!(3); /// Find the maximum 2-digit number by selecting exactly 2 batteries from a bank fn max_joltage_2(bank: &[u8]) -> u64 { - let mut max = 0u64; + let n = bank.len(); - // For each position as the tens digit - for i in 0..bank.len() - 1 { + // Precompute suffix maximums: suffix_max[i] = max digit from position i to end + // 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; - // Find max digit after position i - let units = *bank[i + 1..].iter().max().unwrap() as u64; + let units = suffix_max[i + 1] as u64; // O(1) lookup instead of O(n) scan let joltage = tens * 10 + units; max = max.max(joltage); } @@ -43,7 +50,7 @@ fn max_joltage_k(bank: &[u8], k: usize) -> u64 { for remaining in (1..=k).rev() { // We can pick from [start, n - remaining] inclusive let end = n - remaining; - + // Find the FIRST position of the max digit in this range let mut best_pos = 0; let mut best_digit = bank[start]; @@ -53,7 +60,7 @@ fn max_joltage_k(bank: &[u8], k: usize) -> u64 { best_pos = i; } } - + result = result * 10 + best_digit as u64; start = start + best_pos + 1; }