mirror of
https://github.com/RussellChamp/advent-of-code-2025-cursor.git
synced 2026-10-27 23:52:34 +00:00
Optimize Days 1-8: 41% faster overall (39ms → 23ms)
This commit is contained in:
1 parent
6ff1349679
commit
b067baac3d
10 files changed
+229
-183
No files matched your search
Generated
+3
-2
@@ -26,6 +26,7 @@ dependencies = [
|
|||||||
"itertools",
|
"itertools",
|
||||||
"pico-args",
|
"pico-args",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"smallvec",
|
||||||
"tinyjson",
|
"tinyjson",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -422,9 +423,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.2"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ dhat = { version = "0.3.3", optional = true }
|
|||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
|
smallvec = "1.15.1"
|
||||||
tinyjson = "2.5.1"
|
tinyjson = "2.5.1"
|
||||||
|
|
||||||
# Solution dependencies
|
# Solution dependencies
|
||||||
@@ -11,16 +11,16 @@ 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) | `102.5µs` | `123.5µs` |
|
| [Day 1](./src/bin/01.rs) | `93.9µs` | `113.6µs` |
|
||||||
| [Day 2](./src/bin/02.rs) | `131.1µs` | `314.3µs` |
|
| [Day 2](./src/bin/02.rs) | `125.7µs` | `330.4µs` |
|
||||||
| [Day 3](./src/bin/03.rs) | `85.1µs` | `121.6µs` |
|
| [Day 3](./src/bin/03.rs) | `118.8µs` | `108.4µs` |
|
||||||
| [Day 4](./src/bin/04.rs) | `244.2µs` | `4.1ms` |
|
| [Day 4](./src/bin/04.rs) | `160.6µs` | `838.6µs` |
|
||||||
| [Day 5](./src/bin/05.rs) | `196.7µs` | `66.3µs` |
|
| [Day 5](./src/bin/05.rs) | `53.3µs` | `44.4µs` |
|
||||||
| [Day 6](./src/bin/06.rs) | `109.0µs` | `194.2µs` |
|
| [Day 6](./src/bin/06.rs) | `118.7µs` | `124.8µs` |
|
||||||
| [Day 7](./src/bin/07.rs) | `270.5µs` | `320.2µs` |
|
| [Day 7](./src/bin/07.rs) | `91.8µs` | `72.4µs` |
|
||||||
| [Day 8](./src/bin/08.rs) | `16.9ms` | `16.2ms` |
|
| [Day 8](./src/bin/08.rs) | `11.2ms` | `9.8ms` |
|
||||||
|
|
||||||
**Total: 39.48ms**
|
**Total: 23.40ms**
|
||||||
<!--- benchmarking table --->
|
<!--- benchmarking table --->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-4
@@ -1,5 +1,4 @@
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
advent_of_code::solution!(2);
|
advent_of_code::solution!(2);
|
||||||
|
|
||||||
@@ -106,13 +105,12 @@ pub fn part_two(input: &str) -> Option<u64> {
|
|||||||
let ranges: Vec<(u64, u64)> = input.split(',').filter_map(parse_range).collect();
|
let ranges: Vec<(u64, u64)> = input.split(',').filter_map(parse_range).collect();
|
||||||
let merged = merge_ranges(&ranges);
|
let merged = merge_ranges(&ranges);
|
||||||
|
|
||||||
// Use a small HashSet only for deduplication within merged ranges
|
// Use itertools::unique() for deduplication
|
||||||
// (needed because same number can match multiple pattern lengths)
|
// (needed because same number can match multiple pattern lengths)
|
||||||
let mut seen = HashSet::new();
|
|
||||||
let sum: u64 = merged
|
let sum: u64 = merged
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|&(start, end)| iter_invalid_part2(start, end))
|
.flat_map(|&(start, end)| iter_invalid_part2(start, end))
|
||||||
.filter(|&n| seen.insert(n))
|
.unique()
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
Some(sum)
|
Some(sum)
|
||||||
|
|||||||
+8
-6
@@ -1,12 +1,14 @@
|
|||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
advent_of_code::solution!(3);
|
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: &SmallVec<[u8; 32]>) -> u64 {
|
||||||
let n = bank.len();
|
let n = bank.len();
|
||||||
|
|
||||||
// Precompute suffix maximums: suffix_max[i] = max digit from position i to end
|
// 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²)
|
// This avoids O(n) scan for each position, making overall O(n) instead of O(n²)
|
||||||
let mut suffix_max = vec![0u8; n];
|
let mut suffix_max: SmallVec<[u8; 32]> = SmallVec::from_elem(0, n);
|
||||||
suffix_max[n - 1] = bank[n - 1];
|
suffix_max[n - 1] = bank[n - 1];
|
||||||
for i in (0..n - 1).rev() {
|
for i in (0..n - 1).rev() {
|
||||||
suffix_max[i] = bank[i].max(suffix_max[i + 1]);
|
suffix_max[i] = bank[i].max(suffix_max[i + 1]);
|
||||||
@@ -32,8 +34,8 @@ pub fn part_one(input: &str) -> Option<u64> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert line to digits
|
// Convert line to digits using SmallVec (avoids heap allocation for small inputs)
|
||||||
let digits: Vec<u8> = line
|
let digits: SmallVec<[u8; 32]> = line
|
||||||
.chars()
|
.chars()
|
||||||
.map(|c| c.to_digit(10).unwrap() as u8)
|
.map(|c| c.to_digit(10).unwrap() as u8)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -44,7 +46,7 @@ pub fn part_one(input: &str) -> Option<u64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the maximum k-digit number by selecting exactly k batteries from a bank
|
/// Find the maximum k-digit number by selecting exactly k batteries from a bank
|
||||||
fn max_joltage_k(bank: &[u8], k: usize) -> u64 {
|
fn max_joltage_k(bank: &SmallVec<[u8; 32]>, k: usize) -> u64 {
|
||||||
let n = bank.len();
|
let n = bank.len();
|
||||||
let mut result = 0u64;
|
let mut result = 0u64;
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
@@ -80,7 +82,7 @@ pub fn part_two(input: &str) -> Option<u64> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let digits: Vec<u8> = line
|
let digits: SmallVec<[u8; 32]> = line
|
||||||
.chars()
|
.chars()
|
||||||
.map(|c| c.to_digit(10).unwrap() as u8)
|
.map(|c| c.to_digit(10).unwrap() as u8)
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
+81
-69
@@ -1,72 +1,54 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
advent_of_code::solution!(4);
|
advent_of_code::solution!(4);
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
/// Parse input into a grid of booleans (true = paper)
|
||||||
enum Cell {
|
fn parse_grid(input: &str) -> Vec<Vec<bool>> {
|
||||||
Empty,
|
|
||||||
Paper,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cell {
|
|
||||||
fn from_char(c: char) -> Self {
|
|
||||||
match c {
|
|
||||||
'@' => Cell::Paper,
|
|
||||||
_ => Cell::Empty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_paper(self) -> bool {
|
|
||||||
self == Cell::Paper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse input into a grid of cells
|
|
||||||
fn parse_grid(input: &str) -> Vec<Vec<Cell>> {
|
|
||||||
input
|
input
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| !line.trim().is_empty())
|
.filter(|line| !line.trim().is_empty())
|
||||||
.map(|line| line.chars().map(Cell::from_char).collect())
|
.map(|line| line.chars().map(|c| c == '@').collect())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count adjacent paper rolls (8 directions) for a given position
|
/// Get valid neighbors for a position
|
||||||
fn count_neighbors(grid: &[Vec<Cell>], row: usize, col: usize) -> usize {
|
fn get_neighbors(row: usize, col: usize, rows: usize, cols: usize) -> impl Iterator<Item = (usize, usize)> {
|
||||||
let rows = grid.len() as i32;
|
const DELTAS: [(i32, i32); 8] = [
|
||||||
let cols = grid[0].len() as i32;
|
(-1, -1), (-1, 0), (-1, 1),
|
||||||
let mut count = 0;
|
(0, -1), (0, 1),
|
||||||
|
(1, -1), (1, 0), (1, 1),
|
||||||
|
];
|
||||||
|
|
||||||
for dr in -1..=1 {
|
DELTAS.into_iter().filter_map(move |(dr, dc)| {
|
||||||
for dc in -1..=1 {
|
let nr = row as i32 + dr;
|
||||||
if dr == 0 && dc == 0 {
|
let nc = col as i32 + dc;
|
||||||
continue;
|
if nr >= 0 && nr < rows as i32 && nc >= 0 && nc < cols as i32 {
|
||||||
}
|
Some((nr as usize, nc as usize))
|
||||||
let nr = row as i32 + dr;
|
} else {
|
||||||
let nc = col as i32 + dc;
|
None
|
||||||
if nr >= 0
|
|
||||||
&& nr < rows
|
|
||||||
&& nc >= 0
|
|
||||||
&& nc < cols
|
|
||||||
&& grid[nr as usize][nc as usize].is_paper()
|
|
||||||
{
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a paper roll at (row, col) is accessible (fewer than 4 neighbors)
|
/// Count adjacent paper rolls for a given position
|
||||||
fn is_accessible(grid: &[Vec<Cell>], row: usize, col: usize) -> bool {
|
fn count_neighbors(grid: &[Vec<bool>], row: usize, col: usize) -> u8 {
|
||||||
grid[row][col].is_paper() && count_neighbors(grid, row, col) < 4
|
let rows = grid.len();
|
||||||
|
let cols = grid[0].len();
|
||||||
|
get_neighbors(row, col, rows, cols)
|
||||||
|
.filter(|&(nr, nc)| grid[nr][nc])
|
||||||
|
.count() as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn part_one(input: &str) -> Option<u64> {
|
pub fn part_one(input: &str) -> Option<u64> {
|
||||||
let grid = parse_grid(input);
|
let grid = parse_grid(input);
|
||||||
let mut accessible = 0u64;
|
if grid.is_empty() {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut accessible = 0u64;
|
||||||
for row in 0..grid.len() {
|
for row in 0..grid.len() {
|
||||||
for col in 0..grid[0].len() {
|
for col in 0..grid[0].len() {
|
||||||
if is_accessible(&grid, row, col) {
|
if grid[row][col] && count_neighbors(&grid, row, col) < 4 {
|
||||||
accessible += 1;
|
accessible += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,31 +59,61 @@ pub fn part_one(input: &str) -> Option<u64> {
|
|||||||
|
|
||||||
pub fn part_two(input: &str) -> Option<u64> {
|
pub fn part_two(input: &str) -> Option<u64> {
|
||||||
let mut grid = parse_grid(input);
|
let mut grid = parse_grid(input);
|
||||||
|
if grid.is_empty() {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = grid.len();
|
||||||
|
let cols = grid[0].len();
|
||||||
|
|
||||||
|
// Pre-compute neighbor counts for all cells
|
||||||
|
let mut neighbor_counts: Vec<Vec<u8>> = vec![vec![0; cols]; rows];
|
||||||
|
for row in 0..rows {
|
||||||
|
for col in 0..cols {
|
||||||
|
if grid[row][col] {
|
||||||
|
neighbor_counts[row][col] = count_neighbors(&grid, row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize queue with all accessible cells (paper with < 4 neighbors)
|
||||||
|
let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
|
||||||
|
let mut in_queue: Vec<Vec<bool>> = vec![vec![false; cols]; rows];
|
||||||
|
|
||||||
|
for row in 0..rows {
|
||||||
|
for col in 0..cols {
|
||||||
|
if grid[row][col] && neighbor_counts[row][col] < 4 {
|
||||||
|
queue.push_back((row, col));
|
||||||
|
in_queue[row][col] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut total_removed = 0u64;
|
let mut total_removed = 0u64;
|
||||||
|
|
||||||
loop {
|
while let Some((row, col)) = queue.pop_front() {
|
||||||
// Find all accessible rolls (fewer than 4 neighbors)
|
in_queue[row][col] = false;
|
||||||
let mut to_remove: Vec<(usize, usize)> = Vec::new();
|
|
||||||
|
|
||||||
for row in 0..grid.len() {
|
// Skip if already removed or no longer accessible
|
||||||
for col in 0..grid[0].len() {
|
if !grid[row][col] || neighbor_counts[row][col] >= 4 {
|
||||||
if is_accessible(&grid, row, col) {
|
continue;
|
||||||
to_remove.push((row, col));
|
}
|
||||||
|
|
||||||
|
// Remove this cell
|
||||||
|
grid[row][col] = false;
|
||||||
|
total_removed += 1;
|
||||||
|
|
||||||
|
// Update neighbor counts for adjacent cells and potentially add them to queue
|
||||||
|
for (nr, nc) in get_neighbors(row, col, rows, cols) {
|
||||||
|
if grid[nr][nc] {
|
||||||
|
neighbor_counts[nr][nc] = neighbor_counts[nr][nc].saturating_sub(1);
|
||||||
|
// If this cell became accessible and isn't already queued, add it
|
||||||
|
if neighbor_counts[nr][nc] < 4 && !in_queue[nr][nc] {
|
||||||
|
queue.push_back((nr, nc));
|
||||||
|
in_queue[nr][nc] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no rolls are accessible, we're done
|
|
||||||
if to_remove.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all accessible rolls
|
|
||||||
for (row, col) in &to_remove {
|
|
||||||
grid[*row][*col] = Cell::Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
total_removed += to_remove.len() as u64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(total_removed)
|
Some(total_removed)
|
||||||
|
|||||||
+40
-35
@@ -1,56 +1,61 @@
|
|||||||
advent_of_code::solution!(5);
|
advent_of_code::solution!(5);
|
||||||
|
|
||||||
/// A range of fresh ingredient IDs (inclusive)
|
/// A range of fresh ingredient IDs (inclusive)
|
||||||
|
#[derive(Clone)]
|
||||||
struct FreshRange {
|
struct FreshRange {
|
||||||
start: u64,
|
start: u64,
|
||||||
end: u64,
|
end: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FreshRange {
|
/// Parse the input into fresh ranges and available IDs without extra allocations
|
||||||
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<FreshRange>, Vec<u64>) {
|
fn parse_input(input: &str) -> (Vec<FreshRange>, Vec<u64>) {
|
||||||
// Normalize line endings and split on blank line
|
let mut ranges = Vec::new();
|
||||||
let normalized = input.replace("\r\n", "\n");
|
let mut ids = Vec::new();
|
||||||
let mut sections = normalized.split("\n\n");
|
let mut in_ids_section = false;
|
||||||
|
|
||||||
// Parse fresh ingredient ID ranges
|
for line in input.lines() {
|
||||||
let ranges_section = sections.next().unwrap_or("");
|
let line = line.trim().trim_end_matches('\r');
|
||||||
let ranges: Vec<FreshRange> = ranges_section
|
if line.is_empty() {
|
||||||
.lines()
|
in_ids_section = true;
|
||||||
.filter(|line| !line.trim().is_empty())
|
continue;
|
||||||
.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
|
if in_ids_section {
|
||||||
let ids_section = sections.next().unwrap_or("");
|
if let Ok(id) = line.parse() {
|
||||||
let ids: Vec<u64> = ids_section
|
ids.push(id);
|
||||||
.lines()
|
}
|
||||||
.filter(|line| !line.trim().is_empty())
|
} else if let Some((start_str, end_str)) = line.split_once('-') {
|
||||||
.map(|line| line.trim().parse().unwrap())
|
if let (Ok(start), Ok(end)) = (start_str.parse(), end_str.parse()) {
|
||||||
.collect();
|
ranges.push(FreshRange { start, end });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(ranges, ids)
|
(ranges, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an ingredient ID is fresh (falls within any range)
|
/// Check if an ingredient ID is fresh using binary search on sorted ranges
|
||||||
fn is_fresh(id: u64, ranges: &[FreshRange]) -> bool {
|
fn is_fresh_binary(id: u64, sorted_ranges: &[FreshRange]) -> bool {
|
||||||
ranges.iter().any(|range| range.contains(id))
|
// Binary search to find a range that might contain id
|
||||||
|
// Find the rightmost range where start <= id
|
||||||
|
let idx = sorted_ranges.partition_point(|r| r.start <= id);
|
||||||
|
if idx == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if the range at idx-1 contains id
|
||||||
|
sorted_ranges[idx - 1].end >= id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn part_one(input: &str) -> Option<u64> {
|
pub fn part_one(input: &str) -> Option<u64> {
|
||||||
let (ranges, ids) = parse_input(input);
|
let (mut ranges, ids) = parse_input(input);
|
||||||
|
|
||||||
let fresh_count = ids.iter().filter(|&&id| is_fresh(id, &ranges)).count();
|
// Sort ranges by start for binary search
|
||||||
|
ranges.sort_unstable_by_key(|r| r.start);
|
||||||
|
|
||||||
|
let fresh_count = ids
|
||||||
|
.iter()
|
||||||
|
.filter(|&&id| is_fresh_binary(id, &ranges))
|
||||||
|
.count();
|
||||||
|
|
||||||
Some(fresh_count as u64)
|
Some(fresh_count as u64)
|
||||||
}
|
}
|
||||||
@@ -62,7 +67,7 @@ fn count_unique_fresh_ids(mut ranges: Vec<FreshRange>) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort ranges by start
|
// Sort ranges by start
|
||||||
ranges.sort_by_key(|r| r.start);
|
ranges.sort_unstable_by_key(|r| r.start);
|
||||||
|
|
||||||
// Merge overlapping ranges
|
// Merge overlapping ranges
|
||||||
let mut merged: Vec<FreshRange> = Vec::new();
|
let mut merged: Vec<FreshRange> = Vec::new();
|
||||||
|
|||||||
+11
-6
@@ -116,15 +116,20 @@ fn solve_worksheet_cephalopod(input: &str) -> u64 {
|
|||||||
.unwrap_or('+');
|
.unwrap_or('+');
|
||||||
|
|
||||||
// Read columns right-to-left, each column forms a number
|
// Read columns right-to-left, each column forms a number
|
||||||
|
// Parse digits directly to u64 without String allocation
|
||||||
let numbers: Vec<u64> = (start..end)
|
let numbers: Vec<u64> = (start..end)
|
||||||
.rev()
|
.rev()
|
||||||
.filter_map(|col| {
|
.filter_map(|col| {
|
||||||
let digits: String = number_rows
|
let mut num = 0u64;
|
||||||
.iter()
|
let mut has_digits = false;
|
||||||
.map(|row| row[col])
|
for row in number_rows {
|
||||||
.filter(|c| c.is_ascii_digit())
|
let c = row[col];
|
||||||
.collect();
|
if c.is_ascii_digit() {
|
||||||
digits.parse().ok()
|
num = num * 10 + (c as u64 - '0' as u64);
|
||||||
|
has_digits = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has_digits.then_some(num)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
+34
-26
@@ -1,5 +1,3 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
advent_of_code::solution!(7);
|
advent_of_code::solution!(7);
|
||||||
|
|
||||||
/// Parse the grid and find the starting column
|
/// Parse the grid and find the starting column
|
||||||
@@ -19,7 +17,7 @@ fn parse_grid(input: &str) -> (Vec<Vec<char>>, usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Simulate tachyon beams through the manifold and count splits
|
/// Simulate tachyon beams through the manifold and count splits
|
||||||
/// Beams merge when they occupy the same column (HashSet)
|
/// Uses Vec<bool> instead of HashSet for beam positions
|
||||||
fn count_splits(input: &str) -> u64 {
|
fn count_splits(input: &str) -> u64 {
|
||||||
let (grid, start_col) = parse_grid(input);
|
let (grid, start_col) = parse_grid(input);
|
||||||
if grid.is_empty() {
|
if grid.is_empty() {
|
||||||
@@ -27,33 +25,41 @@ fn count_splits(input: &str) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let width = grid[0].len();
|
let width = grid[0].len();
|
||||||
let mut beams: HashSet<usize> = HashSet::new();
|
let mut beams = vec![false; width];
|
||||||
beams.insert(start_col);
|
let mut new_beams = vec![false; width];
|
||||||
|
beams[start_col] = true;
|
||||||
|
|
||||||
let mut split_count: u64 = 0;
|
let mut split_count: u64 = 0;
|
||||||
|
|
||||||
for row in grid.iter().skip(1) {
|
for row in grid.iter().skip(1) {
|
||||||
let mut new_beams: HashSet<usize> = HashSet::new();
|
new_beams.fill(false);
|
||||||
|
let mut has_beams = false;
|
||||||
|
|
||||||
for &col in &beams {
|
for col in 0..width {
|
||||||
|
if !beams[col] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match row[col] {
|
match row[col] {
|
||||||
'^' => {
|
'^' => {
|
||||||
split_count += 1;
|
split_count += 1;
|
||||||
if col > 0 {
|
if col > 0 {
|
||||||
new_beams.insert(col - 1);
|
new_beams[col - 1] = true;
|
||||||
|
has_beams = true;
|
||||||
}
|
}
|
||||||
if col + 1 < width {
|
if col + 1 < width {
|
||||||
new_beams.insert(col + 1);
|
new_beams[col + 1] = true;
|
||||||
|
has_beams = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
new_beams.insert(col);
|
new_beams[col] = true;
|
||||||
|
has_beams = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beams = new_beams;
|
std::mem::swap(&mut beams, &mut new_beams);
|
||||||
if beams.is_empty() {
|
if !has_beams {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +68,7 @@ fn count_splits(input: &str) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Count the number of unique timelines (paths) through the manifold
|
/// Count the number of unique timelines (paths) through the manifold
|
||||||
/// Each particle at a position represents independent timelines - they don't merge
|
/// Uses Vec<u64> instead of HashMap for particle counts
|
||||||
fn count_timelines(input: &str) -> u64 {
|
fn count_timelines(input: &str) -> u64 {
|
||||||
let (grid, start_col) = parse_grid(input);
|
let (grid, start_col) = parse_grid(input);
|
||||||
if grid.is_empty() {
|
if grid.is_empty() {
|
||||||
@@ -70,40 +76,42 @@ fn count_timelines(input: &str) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let width = grid[0].len();
|
let width = grid[0].len();
|
||||||
|
let mut particles = vec![0u64; width];
|
||||||
// Track number of particles (timelines) at each column position
|
let mut new_particles = vec![0u64; width];
|
||||||
let mut particles: HashMap<usize, u64> = HashMap::new();
|
particles[start_col] = 1;
|
||||||
particles.insert(start_col, 1);
|
|
||||||
|
|
||||||
for row in grid.iter().skip(1) {
|
for row in grid.iter().skip(1) {
|
||||||
let mut new_particles: HashMap<usize, u64> = HashMap::new();
|
new_particles.fill(0);
|
||||||
|
|
||||||
for (&col, &count) in &particles {
|
for col in 0..width {
|
||||||
|
let count = particles[col];
|
||||||
|
if count == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match row[col] {
|
match row[col] {
|
||||||
'^' => {
|
'^' => {
|
||||||
// Each particle splits into 2 timelines (left and right)
|
// Each particle splits into 2 timelines (left and right)
|
||||||
if col > 0 {
|
if col > 0 {
|
||||||
*new_particles.entry(col - 1).or_insert(0) += count;
|
new_particles[col - 1] += count;
|
||||||
}
|
}
|
||||||
if col + 1 < width {
|
if col + 1 < width {
|
||||||
*new_particles.entry(col + 1).or_insert(0) += count;
|
new_particles[col + 1] += count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Particle continues straight down
|
// Particle continues straight down
|
||||||
*new_particles.entry(col).or_insert(0) += count;
|
new_particles[col] += count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
particles = new_particles;
|
std::mem::swap(&mut particles, &mut new_particles);
|
||||||
if particles.is_empty() {
|
if particles.iter().all(|&c| c == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum all particles - each represents a unique timeline
|
particles.iter().sum()
|
||||||
particles.values().sum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn part_one(input: &str) -> Option<u64> {
|
pub fn part_one(input: &str) -> Option<u64> {
|
||||||
|
|||||||
+40
-26
@@ -1,4 +1,4 @@
|
|||||||
use rayon::slice::ParallelSliceMut;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
advent_of_code::solution!(8);
|
advent_of_code::solution!(8);
|
||||||
|
|
||||||
@@ -82,42 +82,47 @@ fn distance_squared(a: (i64, i64, i64), b: (i64, i64, i64)) -> i64 {
|
|||||||
dx * dx + dy * dy + dz * dz
|
dx * dx + dy * dy + dz * dz
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse input and generate sorted pairs (shared between parts)
|
/// Parse coordinates from input
|
||||||
fn generate_sorted_pairs(input: &str) -> (Vec<(i64, i64, i64)>, Vec<(i64, usize, usize)>) {
|
fn parse_coords(input: &str) -> Vec<(i64, i64, i64)> {
|
||||||
let coords: Vec<(i64, i64, i64)> = input
|
input
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| !line.trim().is_empty())
|
.filter(|line| !line.trim().is_empty())
|
||||||
.filter_map(parse_coord)
|
.filter_map(parse_coord)
|
||||||
.collect();
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate all pairs with distances in parallel
|
||||||
|
fn generate_pairs_parallel(coords: &[(i64, i64, i64)]) -> Vec<(i64, usize, usize)> {
|
||||||
let n = coords.len();
|
let n = coords.len();
|
||||||
|
(0..n)
|
||||||
// Generate all pairs with distances
|
.into_par_iter()
|
||||||
let mut pairs: Vec<(i64, usize, usize)> = Vec::with_capacity(n * (n - 1) / 2);
|
.flat_map_iter(|i| {
|
||||||
for i in 0..n {
|
let coords = coords;
|
||||||
for j in (i + 1)..n {
|
((i + 1)..n).map(move |j| {
|
||||||
let dist = distance_squared(coords[i], coords[j]);
|
let dist = distance_squared(coords[i], coords[j]);
|
||||||
pairs.push((dist, i, j));
|
(dist, i, j)
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
|
.collect()
|
||||||
// Parallel sort by distance using rayon
|
|
||||||
pairs.par_sort_unstable_by_key(|&(dist, _, _)| dist);
|
|
||||||
|
|
||||||
(coords, pairs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn part_one(input: &str) -> Option<u64> {
|
pub fn part_one(input: &str) -> Option<u64> {
|
||||||
let (coords, pairs) = generate_sorted_pairs(input);
|
let coords = parse_coords(input);
|
||||||
let n = coords.len();
|
let n = coords.len();
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut uf = UnionFind::new(n);
|
let mut pairs = generate_pairs_parallel(&coords);
|
||||||
|
|
||||||
// Process first 1000 connections
|
// Use partial sort - we only need the 1000 smallest pairs
|
||||||
for &(_, i, j) in pairs.iter().take(1000) {
|
if pairs.len() > 1000 {
|
||||||
|
pairs.select_nth_unstable_by_key(999, |&(dist, _, _)| dist);
|
||||||
|
pairs.truncate(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut uf = UnionFind::new(n);
|
||||||
|
for &(_, i, j) in &pairs {
|
||||||
uf.union(i, j);
|
uf.union(i, j);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,12 +134,17 @@ pub fn part_one(input: &str) -> Option<u64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn part_two(input: &str) -> Option<u64> {
|
pub fn part_two(input: &str) -> Option<u64> {
|
||||||
let (coords, pairs) = generate_sorted_pairs(input);
|
let coords = parse_coords(input);
|
||||||
let n = coords.len();
|
let n = coords.len();
|
||||||
if n <= 1 {
|
if n <= 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut pairs = generate_pairs_parallel(&coords);
|
||||||
|
|
||||||
|
// Full parallel sort needed for part 2 (process in order)
|
||||||
|
pairs.par_sort_unstable_by_key(|&(dist, _, _)| dist);
|
||||||
|
|
||||||
let mut uf = UnionFind::new(n);
|
let mut uf = UnionFind::new(n);
|
||||||
let mut num_circuits = n;
|
let mut num_circuits = n;
|
||||||
let mut last_connection: Option<(usize, usize)> = None;
|
let mut last_connection: Option<(usize, usize)> = None;
|
||||||
@@ -160,10 +170,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_part_one() {
|
fn test_part_one() {
|
||||||
// Example uses 10 connections, not 1000 - test the core logic
|
// Example uses 10 connections, not 1000 - test the core logic
|
||||||
let (coords, pairs) = generate_sorted_pairs(&advent_of_code::template::read_file("examples", DAY));
|
let coords = parse_coords(&advent_of_code::template::read_file("examples", DAY));
|
||||||
let n = coords.len();
|
let n = coords.len();
|
||||||
|
let mut pairs = generate_pairs_parallel(&coords);
|
||||||
|
pairs.select_nth_unstable_by_key(9, |&(dist, _, _)| dist);
|
||||||
|
pairs.truncate(10);
|
||||||
|
|
||||||
let mut uf = UnionFind::new(n);
|
let mut uf = UnionFind::new(n);
|
||||||
for &(_, i, j) in pairs.iter().take(10) {
|
for &(_, i, j) in &pairs {
|
||||||
uf.union(i, j);
|
uf.union(i, j);
|
||||||
}
|
}
|
||||||
let mut sizes = uf.get_circuit_sizes();
|
let mut sizes = uf.get_circuit_sizes();
|
||||||
|
|||||||
Reference in new issue
Block a user