|
| 1 | +use std::{ |
| 2 | + cmp::minmax_by_key, |
| 3 | + collections::{HashMap, HashSet}, |
| 4 | + ops::Index, |
| 5 | +}; |
| 6 | + |
| 7 | +pub fn day12(input: &str) -> (usize, usize) { |
| 8 | + let garden = Garden::new(input); |
| 9 | + |
| 10 | + (part1(&garden), part2(&garden)) |
| 11 | +} |
| 12 | + |
| 13 | +fn part1(garden: &Garden) -> usize { |
| 14 | + garden.iter_regions().map(|r| cost1(&r, &garden)).sum() |
| 15 | +} |
| 16 | + |
| 17 | +fn part2(garden: &Garden) -> usize { |
| 18 | + garden.iter_regions().map(|r| cost2(&r, &garden)).sum() |
| 19 | +} |
| 20 | + |
| 21 | +fn cost1(region: &HashSet<MonadicIndex>, garden: &Garden) -> usize { |
| 22 | + region |
| 23 | + .iter() |
| 24 | + .map(|&i| fences(i, garden).count()) |
| 25 | + .sum::<usize>() |
| 26 | + * region.len() |
| 27 | +} |
| 28 | + |
| 29 | +fn cost2(region: &HashSet<MonadicIndex>, garden: &Garden) -> usize { |
| 30 | + // give each fence an index, keep lookup fast |
| 31 | + let all_fences: HashMap<Fence, usize> = region |
| 32 | + .iter() |
| 33 | + .flat_map(|&i| fences(i, garden)) |
| 34 | + .enumerate() |
| 35 | + .map(|(i, f)| (f, i)) |
| 36 | + .collect(); |
| 37 | + |
| 38 | + let mut uf = UnionFind::new(all_fences.len()); |
| 39 | + for (fence, &index) in all_fences.iter() { |
| 40 | + if let Some(&next) = fence.next(garden).and_then(|f| all_fences.get(&f)) { |
| 41 | + uf.union(index, next); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + let sides = uf.into_sets().count(); |
| 46 | + |
| 47 | + sides * region.len() |
| 48 | +} |
| 49 | + |
| 50 | +fn fences(index: MonadicIndex, garden: &Garden) -> impl Iterator<Item = Fence> + use<'_> { |
| 51 | + let here = garden.to_diadic(index); |
| 52 | + [ |
| 53 | + Direction::Left, |
| 54 | + Direction::Right, |
| 55 | + Direction::Up, |
| 56 | + Direction::Down, |
| 57 | + ] |
| 58 | + .into_iter() |
| 59 | + .filter_map(move |side| { |
| 60 | + let needs_fence = garden |
| 61 | + .neighbor(here, side) |
| 62 | + .map_or(true, |neighbor| garden[here] != garden[neighbor]); |
| 63 | + needs_fence.then_some(Fence { |
| 64 | + location: here, |
| 65 | + side, |
| 66 | + }) |
| 67 | + }) |
| 68 | +} |
| 69 | + |
| 70 | +#[derive(Debug)] |
| 71 | +struct Garden { |
| 72 | + rows: usize, |
| 73 | + cols: usize, |
| 74 | + plots: Vec<u8>, |
| 75 | +} |
| 76 | + |
| 77 | +#[derive(PartialEq, Eq, Hash, Clone, Copy)] |
| 78 | +struct MonadicIndex(usize); |
| 79 | + |
| 80 | +#[derive(PartialEq, Eq, Hash, Clone, Copy)] |
| 81 | +struct DiadicIndex { |
| 82 | + row: usize, |
| 83 | + col: usize, |
| 84 | +} |
| 85 | + |
| 86 | +#[derive(Clone, Copy, PartialEq, Eq, Hash)] |
| 87 | +struct Fence { |
| 88 | + location: DiadicIndex, |
| 89 | + side: Direction, |
| 90 | +} |
| 91 | + |
| 92 | + |
| 93 | +#[derive(Clone, Copy, PartialEq, Eq, Hash)] |
| 94 | +enum Direction { |
| 95 | + Left, |
| 96 | + Right, |
| 97 | + Up, |
| 98 | + Down, |
| 99 | +} |
| 100 | + |
| 101 | +impl Direction { |
| 102 | + fn counter_clockwise(self) -> Direction { |
| 103 | + match self { |
| 104 | + Self::Up => Self::Left, |
| 105 | + Self::Left => Self::Down, |
| 106 | + Self::Down => Self::Right, |
| 107 | + Self::Right => Self::Up, |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +impl Fence { |
| 113 | + // next fence, the direction is defined such that |
| 114 | + // the inside is to the left |
| 115 | + fn next(&self, garden: &Garden) -> Option<Fence> { |
| 116 | + garden |
| 117 | + .neighbor(self.location, self.side.counter_clockwise()) |
| 118 | + .map(|l| Fence { |
| 119 | + location: l, |
| 120 | + side: self.side, |
| 121 | + }) |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +impl Garden { |
| 126 | + fn new(input: &str) -> Self { |
| 127 | + let (rows, cols) = ( |
| 128 | + input.chars().take_while(|&c| c != '\n').count(), |
| 129 | + input.lines().count(), |
| 130 | + ); |
| 131 | + let plots = input |
| 132 | + .lines() |
| 133 | + .flat_map(|l| l.as_bytes().to_owned()) |
| 134 | + .collect(); |
| 135 | + Garden { rows, cols, plots } |
| 136 | + } |
| 137 | + |
| 138 | + fn iter_regions(&self) -> impl Iterator<Item = HashSet<MonadicIndex>> { |
| 139 | + let mut uf = UnionFind::new(self.plots.len()); |
| 140 | + let mut union = |x, y| uf.union(self.to_monadic(x).0, self.to_monadic(y).0); |
| 141 | + |
| 142 | + for i in 0..self.rows * self.cols { |
| 143 | + let here = self.to_diadic(MonadicIndex(i)); |
| 144 | + if let Some(down) = self |
| 145 | + .neighbor(here, Direction::Down) |
| 146 | + .filter(|&down| self[down] == self[here]) |
| 147 | + { |
| 148 | + union(here, down); |
| 149 | + } |
| 150 | + |
| 151 | + if let Some(right) = self |
| 152 | + .neighbor(here, Direction::Right) |
| 153 | + .filter(|&right| self[right] == self[here]) |
| 154 | + { |
| 155 | + union(here, right); |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + uf.into_sets() |
| 160 | + .map(|s| s.into_iter().map(|i| MonadicIndex(i)).collect()) |
| 161 | + } |
| 162 | + |
| 163 | + fn to_monadic(&self, index: DiadicIndex) -> MonadicIndex { |
| 164 | + if index.row >= self.rows || index.col >= self.cols { |
| 165 | + panic!("index out of range"); |
| 166 | + } |
| 167 | + |
| 168 | + MonadicIndex(index.row * self.cols + index.col) |
| 169 | + } |
| 170 | + |
| 171 | + fn to_diadic(&self, index: MonadicIndex) -> DiadicIndex { |
| 172 | + DiadicIndex { |
| 173 | + row: index.0 / self.cols, |
| 174 | + col: index.0 % self.cols, |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + fn neighbor(&self, location: DiadicIndex, direction: Direction) -> Option<DiadicIndex> { |
| 179 | + match direction { |
| 180 | + Direction::Up => (location.row > 0).then(|| DiadicIndex { |
| 181 | + row: location.row - 1, |
| 182 | + ..location |
| 183 | + }), |
| 184 | + Direction::Down => (location.row < self.rows - 1).then(|| DiadicIndex { |
| 185 | + row: location.row + 1, |
| 186 | + ..location |
| 187 | + }), |
| 188 | + Direction::Left => (location.col > 0).then(|| DiadicIndex { |
| 189 | + col: location.col - 1, |
| 190 | + ..location |
| 191 | + }), |
| 192 | + Direction::Right => (location.col < self.cols - 1).then(|| DiadicIndex { |
| 193 | + col: location.col + 1, |
| 194 | + ..location |
| 195 | + }), |
| 196 | + } |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +impl Index<MonadicIndex> for Garden { |
| 201 | + type Output = u8; |
| 202 | + fn index(&self, index: MonadicIndex) -> &Self::Output { |
| 203 | + &self.plots[index.0] |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +impl Index<DiadicIndex> for Garden { |
| 208 | + type Output = u8; |
| 209 | + fn index(&self, index: DiadicIndex) -> &Self::Output { |
| 210 | + &self[self.to_monadic(index)] |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +// partitions a set into disjoint subsets |
| 215 | +// every subset is represented by a node |
| 216 | +struct UnionFind { |
| 217 | + // stores sets as trees, every element |
| 218 | + // contains index of parent node. |
| 219 | + // roots contain their own index |
| 220 | + nodes: Vec<Node>, |
| 221 | +} |
| 222 | + |
| 223 | +#[derive(Clone, Copy, PartialEq, Debug)] |
| 224 | +struct Node { |
| 225 | + // index of the parent, if this points |
| 226 | + // to itself its a root |
| 227 | + parent: usize, |
| 228 | + // number of descendents, |
| 229 | + // only valid for root nodes |
| 230 | + size: usize, |
| 231 | +} |
| 232 | + |
| 233 | +// being generic in the index sucks |
| 234 | +// impl<I: Into<usize> + From<usize> + Copy + PartialEq + PartialOrd + AddAssign> UnionFind<I> { |
| 235 | +impl UnionFind { |
| 236 | + fn new(size: usize) -> Self { |
| 237 | + UnionFind { |
| 238 | + nodes: (0..) |
| 239 | + .take(size) |
| 240 | + .map(|i| Node { parent: i, size: 1 }) |
| 241 | + .collect(), |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + fn find(&mut self, x: usize) -> usize { |
| 246 | + let node = self.nodes[x]; |
| 247 | + if node.parent != x { |
| 248 | + self.nodes[x].parent = self.find(node.parent); |
| 249 | + return self.nodes[x].parent; |
| 250 | + } |
| 251 | + x |
| 252 | + } |
| 253 | + |
| 254 | + fn union(&mut self, x: usize, y: usize) { |
| 255 | + let root_x_idx = self.find(x); |
| 256 | + let root_y_idx = self.find(y); |
| 257 | + |
| 258 | + if root_x_idx == root_y_idx { |
| 259 | + return; |
| 260 | + } |
| 261 | + |
| 262 | + // to prevent trees from becoming too deep, make sure to add the smaller tree to the larger |
| 263 | + let [smaller_idx, larger_idx] = |
| 264 | + minmax_by_key(root_x_idx, root_y_idx, |&idx| self.nodes[idx].size); |
| 265 | + |
| 266 | + self.nodes[smaller_idx].parent = larger_idx; |
| 267 | + self.nodes[larger_idx].size += self.nodes[smaller_idx].size; |
| 268 | + } |
| 269 | + |
| 270 | + fn into_sets(mut self) -> impl Iterator<Item = HashSet<usize>> { |
| 271 | + let mut groups: HashMap<usize, HashSet<usize>> = HashMap::new(); |
| 272 | + |
| 273 | + for i in 0..self.nodes.len() { |
| 274 | + let root = self.find(i); |
| 275 | + groups.entry(root).or_insert_with(HashSet::new).insert(i); |
| 276 | + } |
| 277 | + |
| 278 | + groups.into_values() |
| 279 | + } |
| 280 | +} |
| 281 | + |
| 282 | +// thanks Claude |
| 283 | +#[cfg(test)] |
| 284 | +mod tests { |
| 285 | + use super::*; |
| 286 | + |
| 287 | + #[test] |
| 288 | + fn test_find_root() { |
| 289 | + let mut uf = UnionFind::new(3); |
| 290 | + assert_eq!(uf.find(0), 0); |
| 291 | + assert_eq!(uf.find(1), 1); |
| 292 | + assert_eq!(uf.find(2), 2); |
| 293 | + } |
| 294 | + |
| 295 | + #[test] |
| 296 | + fn test_union_basic() { |
| 297 | + let mut uf = UnionFind::new(4); |
| 298 | + uf.union(0, 1); |
| 299 | + assert_eq!(uf.find(0), uf.find(1)); |
| 300 | + assert_ne!(uf.find(0), uf.find(2)); |
| 301 | + } |
| 302 | + |
| 303 | + #[test] |
| 304 | + fn test_union_multiple() { |
| 305 | + let mut uf = UnionFind::new(5); |
| 306 | + uf.union(0, 1); |
| 307 | + uf.union(1, 2); |
| 308 | + uf.union(3, 4); |
| 309 | + |
| 310 | + assert_eq!(uf.find(0), uf.find(1)); |
| 311 | + assert_eq!(uf.find(1), uf.find(2)); |
| 312 | + assert_eq!(uf.find(3), uf.find(4)); |
| 313 | + assert_ne!(uf.find(0), uf.find(3)); |
| 314 | + } |
| 315 | + |
| 316 | + #[test] |
| 317 | + fn test_union_same_element() { |
| 318 | + let mut uf = UnionFind::new(3); |
| 319 | + uf.union(1, 1); |
| 320 | + assert_eq!(uf.find(1), 1); |
| 321 | + } |
| 322 | + |
| 323 | + #[test] |
| 324 | + fn test_path_compression() { |
| 325 | + let mut uf = UnionFind::new(4); |
| 326 | + uf.union(0, 1); |
| 327 | + uf.union(1, 2); |
| 328 | + uf.union(2, 3); |
| 329 | + |
| 330 | + // First find should trigger path compression |
| 331 | + let root = uf.find(3); |
| 332 | + // All nodes should now point directly to root |
| 333 | + assert_eq!(uf.nodes[0].parent, root); |
| 334 | + assert_eq!(uf.nodes[1].parent, root); |
| 335 | + assert_eq!(uf.nodes[2].parent, root); |
| 336 | + assert_eq!(uf.nodes[3].parent, root); |
| 337 | + } |
| 338 | + |
| 339 | + #[test] |
| 340 | + fn test_disjoint_sets() { |
| 341 | + let mut uf = UnionFind::new(6); |
| 342 | + uf.union(0, 1); |
| 343 | + uf.union(2, 3); |
| 344 | + uf.union(4, 5); |
| 345 | + |
| 346 | + // Three separate components |
| 347 | + assert_ne!(uf.find(0), uf.find(2)); |
| 348 | + assert_ne!(uf.find(0), uf.find(4)); |
| 349 | + assert_ne!(uf.find(2), uf.find(4)); |
| 350 | + } |
| 351 | + |
| 352 | + #[test] |
| 353 | + fn test_union_find_into_sets() { |
| 354 | + let mut uf = UnionFind::new(6); |
| 355 | + |
| 356 | + // Create some unions: {0,1,2}, {3,4}, {5} |
| 357 | + uf.union(0, 1); |
| 358 | + uf.union(1, 2); |
| 359 | + uf.union(3, 4); |
| 360 | + |
| 361 | + let sets: Vec<HashSet<usize>> = uf.into_sets().collect(); |
| 362 | + |
| 363 | + // Should have 3 sets |
| 364 | + assert_eq!(sets.len(), 3); |
| 365 | + |
| 366 | + // Check that we have the expected sets (order doesn't matter) |
| 367 | + let mut found_sets = Vec::new(); |
| 368 | + for set in sets { |
| 369 | + found_sets.push(set); |
| 370 | + } |
| 371 | + |
| 372 | + // Sort by size for consistent testing |
| 373 | + found_sets.sort_by_key(|s| s.len()); |
| 374 | + |
| 375 | + assert_eq!(found_sets[0], HashSet::from([5])); // singleton |
| 376 | + assert_eq!(found_sets[1], HashSet::from([3, 4])); // pair |
| 377 | + assert_eq!(found_sets[2], HashSet::from([0, 1, 2])); // triple |
| 378 | + } |
| 379 | +} |
0 commit comments