Optimize Days 1-8: 41% faster overall (39ms → 23ms)

This commit is contained in:
Russell Champoux committed 2025-12-09 16:16:10 -05:00
1 parent 6ff1349679
commit b067baac3d
10 files changed
+229 -183

No files matched your search

Generated
+3 -2
View File
@@ -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"
+1
View File
@@ -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
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();