Advent of Code solutions in Rust

feat: day 9 of AoC 2025

Not my proudest moment in AoC history... but it
works, albeit slowly.

+338 -2
+1
Cargo.lock
··· 151 151 "itertools", 152 152 "ndarray", 153 153 "num-traits", 154 + "rayon", 154 155 "thiserror", 155 156 "tokio", 156 157 ]
+1
aoc_2025/Cargo.toml
··· 12 12 itertools = { workspace = true } 13 13 ndarray = { workspace = true } 14 14 num-traits = { workspace = true } 15 + rayon = { workspace = true } 15 16 thiserror = { workspace = true } 16 17 tokio = { workspace = true }
+329
aoc_2025/src/day09.rs
··· 1 + use std::collections::BTreeSet; 2 + 3 + use aoc_companion::prelude::*; 4 + use aoc_utils::{ 5 + iter::AtMostThree, 6 + linalg::{ParseVectorError, Vector}, 7 + }; 8 + use itertools::Itertools as _; 9 + use rayon::prelude::*; 10 + 11 + pub(crate) struct Door { 12 + tiles: Vec<Vector<i32, 2>>, 13 + } 14 + 15 + impl<'input> Solution<'input> for Door { 16 + fn parse(input: &'input str) -> Result<Self, ParseVectorError<std::num::ParseIntError>> { 17 + input 18 + .lines() 19 + .map(str::parse) 20 + .try_collect() 21 + .map(|tiles| Door { tiles }) 22 + } 23 + 24 + fn part1(&self) -> u64 { 25 + max_area(&self.tiles) 26 + } 27 + 28 + fn part2(&self) -> u64 { 29 + max_area_contained(&self.tiles) 30 + } 31 + } 32 + 33 + fn max_area(tiles: &[Vector<i32, 2>]) -> u64 { 34 + tiles 35 + .iter() 36 + .copied() 37 + .tuple_combinations() 38 + .map(rect_area) 39 + .max() 40 + .expect("at least two tiles are required") 41 + } 42 + 43 + fn max_area_contained(tiles: &[Vector<i32, 2>]) -> u64 { 44 + let mapping_x = find_mapping(tiles.iter().map(|v| v[0])); 45 + let mapping_y = find_mapping(tiles.iter().map(|v| v[1])); 46 + let mapping_slices = (mapping_x.as_slice(), mapping_y.as_slice()); 47 + 48 + tiles 49 + .iter() 50 + .copied() 51 + .tuple_combinations() 52 + .sorted_by_key(|rect| rect_area(*rect)) 53 + .collect_vec() 54 + .into_par_iter() 55 + .rev() 56 + .find_first(|rect| { 57 + rect_points( 58 + deflate(rect.0, mapping_slices), 59 + deflate(rect.1, mapping_slices), 60 + ) 61 + .all(|p| is_point_in_polygon(inflate(p, mapping_slices), tiles)) 62 + }) 63 + .inspect(|rect| { 64 + dbg!(rect); 65 + }) 66 + .map(rect_area) 67 + .expect("at least two tiles are required") 68 + } 69 + 70 + fn rect_area((a, b): (Vector<i32, 2>, Vector<i32, 2>)) -> u64 { 71 + let diff = b - a; 72 + (diff[0].unsigned_abs() as u64 + 1) * (diff[1].unsigned_abs() as u64 + 1) 73 + } 74 + 75 + fn rect_points(a: Vector<usize, 2>, b: Vector<usize, 2>) -> impl Iterator<Item = Vector<usize, 2>> { 76 + (a[0].min(b[0])..=a[0].max(b[0])) 77 + .cartesian_product(a[1].min(b[1])..=a[1].max(b[1])) 78 + .map(|(x, y)| Vector([x, y])) 79 + } 80 + 81 + fn is_point_in_polygon(point: Vector<i32, 2>, polygon: &[Vector<i32, 2>]) -> bool { 82 + is_on_polygon_boundary(point, polygon) 83 + || polygon 84 + .iter() 85 + .copied() 86 + .circular_tuple_windows() 87 + .filter(|segment| is_right_of_intersection(point, *segment)) 88 + .count() 89 + % 2 90 + == 1 91 + } 92 + 93 + fn is_on_polygon_boundary(point: Vector<i32, 2>, polygon: &[Vector<i32, 2>]) -> bool { 94 + polygon 95 + .iter() 96 + .copied() 97 + .circular_tuple_windows() 98 + .any(|segment: (_, _)| { 99 + if segment.0[1] == segment.1[1] { 100 + point[1] == segment.0[1] 101 + && (segment.0[0].min(segment.1[0])..=segment.0[0].max(segment.1[0])) 102 + .contains(&point[0]) 103 + } else if segment.0[0] == segment.1[0] { 104 + point[0] == segment.0[0] 105 + && (segment.0[1].min(segment.1[1])..=segment.0[1].max(segment.1[1])) 106 + .contains(&point[1]) 107 + } else { 108 + panic!("non-axis-parallel lines are not supported"); 109 + } 110 + }) 111 + } 112 + 113 + fn is_right_of_intersection( 114 + point: Vector<i32, 2>, 115 + segment: (Vector<i32, 2>, Vector<i32, 2>), 116 + ) -> bool { 117 + if segment.0[1] == segment.1[1] { 118 + false 119 + } else if segment.0[0] == segment.1[0] { 120 + point[0] > segment.0[0] 121 + && ((segment.0[1]..segment.1[1]).contains(&point[1]) 122 + || (segment.1[1]..segment.0[1]).contains(&point[1])) 123 + } else { 124 + panic!("non-axis-parallel lines are not supported"); 125 + } 126 + } 127 + 128 + fn find_mapping(coords: impl IntoIterator<Item = i32>) -> Vec<i32> { 129 + BTreeSet::from_iter(coords) 130 + .iter() 131 + .circular_tuple_windows() 132 + .flat_map(|(&a, &b)| { 133 + if a + 1 == b { 134 + AtMostThree::two(a, b) 135 + } else { 136 + AtMostThree::three(a, a + 1, b) 137 + } 138 + }) 139 + .collect() 140 + } 141 + 142 + fn deflate(v: Vector<i32, 2>, mapping: (&[i32], &[i32])) -> Vector<usize, 2> { 143 + Vector([ 144 + mapping.0.binary_search(&v[0]).unwrap(), 145 + mapping.1.binary_search(&v[1]).unwrap(), 146 + ]) 147 + } 148 + 149 + fn inflate(v: Vector<usize, 2>, mapping: (&[i32], &[i32])) -> Vector<i32, 2> { 150 + Vector([mapping.0[v[0]], mapping.1[v[1]]]) 151 + } 152 + 153 + #[cfg(test)] 154 + mod tests { 155 + use std::collections::HashSet; 156 + 157 + use aoc_utils::geometry::Point; 158 + 159 + use super::*; 160 + 161 + const EXAMPLE_TILES: &[Vector<i32, 2>] = &[ 162 + Vector([7, 1]), 163 + Vector([11, 1]), 164 + Vector([11, 7]), 165 + Vector([9, 7]), 166 + Vector([9, 5]), 167 + Vector([2, 5]), 168 + Vector([2, 3]), 169 + Vector([7, 3]), 170 + ]; 171 + 172 + const EXAMPLE_MAP: &str = "\ 173 + .............. 174 + .......#XXX#.. 175 + .......XXXXX.. 176 + ..#XXXX#XXXX.. 177 + ..XXXXXXXXXX.. 178 + ..#XXXXXX#XX.. 179 + .........XXX.. 180 + .........#X#.. 181 + .............."; 182 + 183 + const PATHOLOGICAL_TILES: &[Vector<i32, 2>] = &[ 184 + Vector([1, 1]), 185 + Vector([5, 1]), 186 + Vector([5, 6]), 187 + Vector([8, 6]), 188 + Vector([8, 1]), 189 + Vector([10, 1]), 190 + Vector([10, 5]), 191 + Vector([12, 5]), 192 + Vector([12, 7]), 193 + Vector([3, 7]), 194 + Vector([3, 3]), 195 + Vector([1, 3]), 196 + ]; 197 + 198 + const PATHOLOGICAL_MAP: &str = "\ 199 + .............. 200 + .#XXX#..#X#... 201 + .XXXXX..XXX... 202 + .#X#XX..XXX... 203 + ...XXX..XXX... 204 + ...XXX..XX#X#. 205 + ...XX#XX#XXXX. 206 + ...#XXXXXXXX#. 207 + .............."; 208 + 209 + const NARROW_TILES: &[Vector<i32, 2>] = &[ 210 + Vector([1, 1]), 211 + Vector([7, 1]), 212 + Vector([7, 3]), 213 + Vector([3, 3]), 214 + Vector([3, 4]), 215 + Vector([7, 4]), 216 + Vector([7, 6]), 217 + Vector([1, 6]), 218 + ]; 219 + 220 + const NARROW_MAP: &str = "\ 221 + ......... 222 + .#XXXXX#. 223 + .XXXXXXX. 224 + .XX#XXX#. 225 + .XX#XXX#. 226 + .XXXXXXX. 227 + .#XXXXX#. 228 + ........."; 229 + 230 + const ANVIL_TILES: &[Vector<i32, 2>] = &[ 231 + Vector([1, 1]), 232 + Vector([14, 1]), 233 + Vector([14, 12]), 234 + Vector([1, 12]), 235 + Vector([1, 7]), 236 + Vector([7, 7]), 237 + Vector([7, 8]), 238 + Vector([2, 8]), 239 + Vector([2, 11]), 240 + Vector([13, 11]), 241 + Vector([13, 8]), 242 + Vector([8, 8]), 243 + Vector([8, 5]), 244 + Vector([13, 5]), 245 + Vector([13, 2]), 246 + Vector([2, 2]), 247 + Vector([2, 5]), 248 + Vector([7, 5]), 249 + Vector([7, 6]), 250 + Vector([1, 6]), 251 + ]; 252 + 253 + const ANVIL_MAP: &str = "\ 254 + ................ 255 + .#XXXXXXXXXXXX#. 256 + .X#XXXXXXXXXX#X. 257 + .XX..........XX. 258 + .XX..........XX. 259 + .X#XXXX##XXXX#X. 260 + .#XXXXX#XXXXXXX. 261 + .#XXXXX#XXXXXXX. 262 + .X#XXXX##XXXX#X. 263 + .XX..........XX. 264 + .XX..........XX. 265 + .X#XXXXXXXXXX#X. 266 + .#XXXXXXXXXXXX#. 267 + ................"; 268 + 269 + #[test] 270 + fn max_area_spanned_by_tiles() { 271 + assert_eq!(max_area(EXAMPLE_TILES), 50); 272 + } 273 + 274 + #[test] 275 + fn max_area_contained_by_tiles() { 276 + assert_eq!(max_area_contained(EXAMPLE_TILES), 24); 277 + assert_eq!(max_area_contained(PATHOLOGICAL_TILES), 21); 278 + assert_eq!(max_area_contained(NARROW_TILES), 42); 279 + assert_eq!(max_area_contained(ANVIL_TILES), 48); 280 + } 281 + 282 + fn test_point_in_polygon(tiles: &[Vector<i32, 2>], map: &str) { 283 + let polygon_points: HashSet<Vector<i32, 2>> = aoc_utils::geometry::parse_ascii_map(map) 284 + .unwrap() 285 + .indexed_iter() 286 + .filter(|(_, b)| **b != b'.') 287 + .map(|((y, x), _)| Vector([x as i32, y as i32])) 288 + .collect(); 289 + let polygon_neighbors: HashSet<Vector<i32, 2>> = polygon_points 290 + .iter() 291 + .flat_map(|p| p.neighbors()) 292 + .filter(|p| !polygon_points.contains(p)) 293 + .collect(); 294 + 295 + for p in polygon_points { 296 + assert!( 297 + is_point_in_polygon(p, tiles), 298 + "{p:?} is in the polygon, but is_point_in_polygon said otherwise" 299 + ); 300 + } 301 + 302 + for p in polygon_neighbors { 303 + assert!( 304 + !is_point_in_polygon(p, tiles), 305 + "{p:?} is NOT in the polygon, but is_point_in_polygon said otherwise" 306 + ); 307 + } 308 + } 309 + 310 + #[test] 311 + fn point_in_example_polygon() { 312 + test_point_in_polygon(EXAMPLE_TILES, EXAMPLE_MAP); 313 + } 314 + 315 + #[test] 316 + fn point_in_pathological_polygon() { 317 + test_point_in_polygon(PATHOLOGICAL_TILES, PATHOLOGICAL_MAP); 318 + } 319 + 320 + #[test] 321 + fn point_in_narrow_polygon() { 322 + test_point_in_polygon(NARROW_TILES, NARROW_MAP); 323 + } 324 + 325 + #[test] 326 + fn point_in_anvil_polygon() { 327 + test_point_in_polygon(ANVIL_TILES, ANVIL_MAP); 328 + } 329 + }
+2 -2
aoc_2025/src/main.rs
··· 8 8 mod day06; 9 9 mod day07; 10 10 mod day08; 11 - // mod day09; 11 + mod day09; 12 12 // mod day10; 13 13 // mod day11; 14 14 // mod day12; ··· 26 26 door!(2025-12-06 ~> day06), 27 27 door!(2025-12-07 ~> day07), 28 28 door!(2025-12-08 ~> day08), 29 - // door!(2025-12-09 ~> day09), 29 + door!(2025-12-09 ~> day09), 30 30 // door!(2025-12-10 ~> day10), 31 31 // door!(2025-12-11 ~> day11), 32 32 // door!(2025-12-12 ~> day12),
+5
aoc_utils/src/iter.rs
··· 78 78 pub struct Few<T, const N: usize>([Option<T>; N]); 79 79 80 80 pub type AtMostTwo<T> = Few<T, 2>; 81 + pub type AtMostThree<T> = Few<T, 3>; 81 82 82 83 impl<T, const N: usize> Few<T, N> { 83 84 pub fn new<const M: usize>(items: [T; M]) -> Self { ··· 102 103 103 104 pub fn two(item1: T, item2: T) -> Self { 104 105 Few::new([item1, item2]) 106 + } 107 + 108 + pub fn three(item1: T, item2: T, item3: T) -> Self { 109 + Few::new([item1, item2, item3]) 105 110 } 106 111 } 107 112