diff --git a/.cursor/rules/use-preferred-crates.mdc b/.cursor/rules/use-preferred-crates.mdc new file mode 100644 index 0000000..3de6bc5 --- /dev/null +++ b/.cursor/rules/use-preferred-crates.mdc @@ -0,0 +1,4 @@ +--- +alwaysApply: true +--- +When generating code, consider utilizing libraries that have been added to the project (such as itertools) if they make the resulting code more performant or readable. diff --git a/README.md b/README.md index 17b98a2..d82020c 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. | [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` | +| [Day 6](./src/bin/06.rs) | `109.0µs` | `194.2µs` | -**Total: 5.49ms** +**Total: 5.79ms** --- diff --git a/data/examples/06.txt b/data/examples/06.txt new file mode 100644 index 0000000..6bd9e05 --- /dev/null +++ b/data/examples/06.txt @@ -0,0 +1,5 @@ +123 328 51 64 + 45 64 387 23 + 6 98 215 314 +* + * + + diff --git a/data/examples/08.txt b/data/examples/08.txt new file mode 100644 index 0000000..458322a --- /dev/null +++ b/data/examples/08.txt @@ -0,0 +1,21 @@ +162,817,812 +57,618,57 +906,360,560 +592,479,940 +352,342,300 +466,668,158 +542,29,236 +431,825,988 +739,650,466 +52,470,668 +216,146,977 +819,987,18 +117,168,530 +805,96,715 +346,949,466 +970,615,88 +941,993,340 +862,61,35 +984,92,344 +425,690,689 + diff --git a/src/bin/06.rs b/src/bin/06.rs new file mode 100644 index 0000000..37054bd --- /dev/null +++ b/src/bin/06.rs @@ -0,0 +1,155 @@ +use itertools::Itertools; + +advent_of_code::solution!(6); + +/// Calculate result for a problem given numbers and operator +fn calculate_result(numbers: &[u64], operator: &str) -> u64 { + match operator { + "*" => numbers.iter().product(), + "+" => numbers.iter().sum(), + _ => 0, + } +} + +/// Parse problems from the worksheet and calculate the grand total +fn solve_worksheet(input: &str) -> u64 { + let lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect(); + if lines.is_empty() { + return 0; + } + + // Parse each line into tokens using whitespace splitting + let rows: Vec> = lines + .iter() + .map(|line| line.split_ascii_whitespace().collect()) + .collect(); + + // Split into operator row and number rows + let (number_rows, operator_row) = rows.split_at(rows.len() - 1); + let operator_row = &operator_row[0]; + + // Process each problem column and sum results + (0..operator_row.len()) + .map(|problem_idx| { + let operator = operator_row.get(problem_idx).copied().unwrap_or("+"); + let numbers: Vec = number_rows + .iter() + .filter_map(|row| row.get(problem_idx)) + .filter_map(|s| s.parse().ok()) + .collect(); + calculate_result(&numbers, operator) + }) + .sum() +} + +pub fn part_one(input: &str) -> Option { + Some(solve_worksheet(input)) +} + +/// Calculate result for a problem given numbers and operator (char version) +fn calculate_result_char(numbers: &[u64], operator: char) -> u64 { + match operator { + '*' => numbers.iter().product(), + '+' => numbers.iter().sum(), + _ => 0, + } +} + +/// Find problem boundaries from column space information +/// Returns ranges of (start, end) for each problem +fn find_problem_boundaries(grid: &[Vec], max_len: usize) -> Vec<(usize, usize)> { + (0..max_len) + .map(|col| grid.iter().all(|row| row[col] == ' ')) + .enumerate() + .chunk_by(|&(_, is_space)| is_space) + .into_iter() + .filter_map(|(is_space, group)| { + if is_space { + None + } else { + let cols: Vec<_> = group.collect(); + let start = cols.first()?.0; + let end = cols.last()?.0 + 1; + Some((start, end)) + } + }) + .collect() +} + +/// Parse problems using cephalopod math (right-to-left columns) +/// This requires parsing the actual character grid to preserve column positions +fn solve_worksheet_cephalopod(input: &str) -> u64 { + let lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect(); + if lines.is_empty() { + return 0; + } + + // Find max line length and build padded grid + let max_len = lines.iter().map(|l| l.len()).max().unwrap_or(0); + let grid: Vec> = lines + .iter() + .map(|l| { + let mut chars: Vec = l.chars().collect(); + chars.resize(max_len, ' '); + chars + }) + .collect(); + + if grid.len() < 2 { + return 0; + } + + // Split into operator row and number rows + let (number_rows, operator_rows) = grid.split_at(grid.len() - 1); + let operator_row = &operator_rows[0]; + + // Find problem boundaries and process each + find_problem_boundaries(&grid, max_len) + .into_iter() + .map(|(start, end)| { + // Find operator for this problem + let operator = (start..end) + .find_map(|col| { + let c = operator_row[col]; + (c == '*' || c == '+').then_some(c) + }) + .unwrap_or('+'); + + // Read columns right-to-left, each column forms a number + let numbers: Vec = (start..end) + .rev() + .filter_map(|col| { + let digits: String = number_rows + .iter() + .map(|row| row[col]) + .filter(|c| c.is_ascii_digit()) + .collect(); + digits.parse().ok() + }) + .collect(); + + calculate_result_char(&numbers, operator) + }) + .sum() +} + +pub fn part_two(input: &str) -> Option { + Some(solve_worksheet_cephalopod(input)) +} + +#[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(4277556)); + } + + #[test] + fn test_part_two() { + let result = part_two(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(3263827)); + } +} diff --git a/src/bin/08.rs b/src/bin/08.rs new file mode 100644 index 0000000..96820af --- /dev/null +++ b/src/bin/08.rs @@ -0,0 +1,152 @@ +advent_of_code::solution!(8); + +/// Union-Find data structure for tracking circuits +struct UnionFind { + parent: Vec, + rank: Vec, + size: Vec, +} + +impl UnionFind { + fn new(n: usize) -> Self { + Self { + parent: (0..n).collect(), + rank: vec![0; n], + size: vec![1; n], + } + } + + fn find(&mut self, x: usize) -> usize { + if self.parent[x] != x { + self.parent[x] = self.find(self.parent[x]); // Path compression + } + self.parent[x] + } + + fn union(&mut self, x: usize, y: usize) -> bool { + let root_x = self.find(x); + let root_y = self.find(y); + + if root_x == root_y { + return false; // Already in same circuit + } + + // Union by rank + match self.rank[root_x].cmp(&self.rank[root_y]) { + std::cmp::Ordering::Less => { + self.parent[root_x] = root_y; + self.size[root_y] += self.size[root_x]; + } + std::cmp::Ordering::Greater => { + self.parent[root_y] = root_x; + self.size[root_x] += self.size[root_y]; + } + std::cmp::Ordering::Equal => { + self.parent[root_y] = root_x; + self.size[root_x] += self.size[root_y]; + self.rank[root_x] += 1; + } + } + true + } + + fn get_circuit_sizes(&mut self) -> Vec { + let n = self.parent.len(); + let mut sizes = Vec::new(); + for i in 0..n { + if self.find(i) == i { + sizes.push(self.size[i]); + } + } + sizes + } +} + +/// Parse a line "X,Y,Z" into coordinates +fn parse_coord(line: &str) -> Option<(i64, i64, i64)> { + let mut parts = line.split(','); + let x = parts.next()?.parse().ok()?; + let y = parts.next()?.parse().ok()?; + let z = parts.next()?.parse().ok()?; + Some((x, y, z)) +} + +/// Calculate squared Euclidean distance (to avoid sqrt) +fn distance_squared(a: (i64, i64, i64), b: (i64, i64, i64)) -> i64 { + let dx = a.0 - b.0; + let dy = a.1 - b.1; + let dz = a.2 - b.2; + dx * dx + dy * dy + dz * dz +} + +fn solve(input: &str, num_connections: usize) -> Option { + let coords: Vec<(i64, i64, i64)> = input + .lines() + .filter(|line| !line.trim().is_empty()) + .filter_map(parse_coord) + .collect(); + + let n = coords.len(); + if n == 0 { + return None; + } + + // Generate all pairs with distances + let mut pairs: Vec<(i64, usize, usize)> = Vec::with_capacity(n * (n - 1) / 2); + for i in 0..n { + for j in (i + 1)..n { + let dist = distance_squared(coords[i], coords[j]); + pairs.push((dist, i, j)); + } + } + + // Sort by distance + pairs.sort_unstable_by_key(|&(dist, _, _)| dist); + + // Process connections using Union-Find + let mut uf = UnionFind::new(n); + let mut connections_made = 0; + + for (_, i, j) in pairs { + if connections_made >= num_connections { + break; + } + // Try to connect - even if already connected, it counts as one of our connections + uf.union(i, j); + connections_made += 1; + } + + // Get circuit sizes and find 3 largest + let mut sizes = uf.get_circuit_sizes(); + sizes.sort_unstable_by(|a, b| b.cmp(a)); // Sort descending + + // Multiply 3 largest + let result: u64 = sizes.iter().take(3).map(|&s| s as u64).product(); + Some(result) +} + +pub fn part_one(input: &str) -> Option { + solve(input, 1000) +} + +pub fn part_two(input: &str) -> Option { + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + // Example uses 10 connections, not 1000 + let result = solve(&advent_of_code::template::read_file("examples", DAY), 10); + assert_eq!(result, Some(40)); + } + + #[test] + fn test_part_two() { + let result = part_two(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, None); + } +}