|
| 1 | +use std::{collections::HashSet, convert::identity}; |
| 2 | + |
| 3 | +use aoc_lib::{direction::cardinal::Direction, matrix::Matrix}; |
| 4 | +use common::{solution, Answer}; |
| 5 | +use nd_vec::Vec2; |
| 6 | + |
| 7 | +solution!("Garden Groups", 12); |
| 8 | + |
| 9 | +fn part_a(input: &str) -> Answer { |
| 10 | + let mut garden = Garden::parse(input); |
| 11 | + |
| 12 | + let mut sum = 0; |
| 13 | + |
| 14 | + for pos in garden.matrix.clone().iter().map(|(pos, _)| pos) { |
| 15 | + let (area, perimeter) = garden.flood(pos); |
| 16 | + sum += area * perimeter; |
| 17 | + } |
| 18 | + |
| 19 | + sum.into() |
| 20 | +} |
| 21 | + |
| 22 | +fn part_b(input: &str) -> Answer { |
| 23 | + let mut garden = Garden::parse(input); |
| 24 | + |
| 25 | + let mut sum = 0; |
| 26 | + |
| 27 | + for pos in garden.matrix.clone().iter().map(|(pos, _)| pos) { |
| 28 | + let plant = *garden.matrix.get(pos).unwrap(); |
| 29 | + let (area, perimeter) = garden.flood_b(pos); |
| 30 | + if perimeter.is_empty() { |
| 31 | + continue; |
| 32 | + } |
| 33 | + |
| 34 | + let mut corners = 0; |
| 35 | + |
| 36 | + for &point in area.iter() { |
| 37 | + for (a, b) in [ |
| 38 | + (Direction::Up, Direction::Right), |
| 39 | + (Direction::Right, Direction::Down), |
| 40 | + (Direction::Down, Direction::Left), |
| 41 | + (Direction::Left, Direction::Up), |
| 42 | + ] { |
| 43 | + // if a and b are both not in area +1 |
| 44 | + if !area.contains(&a.advance(point)) && !area.contains(&b.advance(point)) { |
| 45 | + corners += 1; |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + for (a, b) in [ |
| 50 | + (Direction::Up, Direction::Right), |
| 51 | + (Direction::Right, Direction::Down), |
| 52 | + (Direction::Down, Direction::Left), |
| 53 | + (Direction::Left, Direction::Up), |
| 54 | + ] { |
| 55 | + let e = a.as_vector::<i32>() + b.as_vector(); |
| 56 | + if area.contains(&a.advance(point)) |
| 57 | + && area.contains(&b.advance(point)) |
| 58 | + && !area.contains(&(point + e)) |
| 59 | + { |
| 60 | + corners += 1; |
| 61 | + } |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + println!("{} * {corners} [{plant}]", area.len()); |
| 66 | + sum += area.len() * corners; |
| 67 | + } |
| 68 | + |
| 69 | + sum.into() |
| 70 | +} |
| 71 | + |
| 72 | +struct Garden { |
| 73 | + matrix: Matrix<char>, |
| 74 | + |
| 75 | + seen: HashSet<Vec2<usize>>, |
| 76 | +} |
| 77 | + |
| 78 | +impl Garden { |
| 79 | + fn parse(input: &str) -> Self { |
| 80 | + let matrix = Matrix::new_chars(input, identity); |
| 81 | + Self { |
| 82 | + matrix, |
| 83 | + seen: HashSet::new(), |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + // -> (area, perimeter) |
| 88 | + fn flood(&mut self, start: Vec2<usize>) -> (u32, u32) { |
| 89 | + let (mut area, mut perimeter) = (1, 0); |
| 90 | + let plant = self.matrix.get(start).unwrap(); |
| 91 | + |
| 92 | + let mut queue = Vec::new(); |
| 93 | + |
| 94 | + if !self.seen.insert(start) { |
| 95 | + return (0, 0); |
| 96 | + } |
| 97 | + queue.push(start); |
| 98 | + |
| 99 | + while let Some(pos) = queue.pop() { |
| 100 | + for dir in Direction::ALL.into_iter() { |
| 101 | + let Some(next) = dir.try_advance(pos) else { |
| 102 | + perimeter += 1; |
| 103 | + continue; |
| 104 | + }; |
| 105 | + if !self.matrix.contains(next) { |
| 106 | + perimeter += 1; |
| 107 | + continue; |
| 108 | + } |
| 109 | + |
| 110 | + if self.matrix.get(next).unwrap() == plant { |
| 111 | + if self.seen.insert(next) { |
| 112 | + area += 1; |
| 113 | + queue.push(next); |
| 114 | + } |
| 115 | + } else { |
| 116 | + perimeter += 1 |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + (area, perimeter) |
| 122 | + } |
| 123 | + |
| 124 | + fn flood_b(&mut self, start: Vec2<usize>) -> (HashSet<Vec2<i32>>, HashSet<Vec2<i32>>) { |
| 125 | + let (mut area, mut perimeter) = (HashSet::new(), HashSet::new()); |
| 126 | + area.insert(start.try_cast::<i32>().unwrap()); |
| 127 | + let plant = self.matrix.get(start).unwrap(); |
| 128 | + |
| 129 | + let mut queue = Vec::new(); |
| 130 | + |
| 131 | + if !self.seen.insert(start) { |
| 132 | + return (HashSet::new(), HashSet::new()); |
| 133 | + } |
| 134 | + queue.push(start); |
| 135 | + |
| 136 | + while let Some(pos) = queue.pop() { |
| 137 | + for dir in Direction::ALL.into_iter() { |
| 138 | + let Some(next) = dir.try_advance(pos) else { |
| 139 | + perimeter.insert(dir.advance(pos.try_cast::<i32>().unwrap())); |
| 140 | + continue; |
| 141 | + }; |
| 142 | + if !self.matrix.contains(next) { |
| 143 | + perimeter.insert(next.try_cast::<i32>().unwrap()); |
| 144 | + continue; |
| 145 | + } |
| 146 | + |
| 147 | + if self.matrix.get(next).unwrap() == plant { |
| 148 | + if self.seen.insert(next) { |
| 149 | + area.insert(next.try_cast::<i32>().unwrap()); |
| 150 | + queue.push(next); |
| 151 | + } |
| 152 | + } else { |
| 153 | + perimeter.insert(next.try_cast::<i32>().unwrap()); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + (area, perimeter) |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +#[cfg(test)] |
| 163 | +mod test { |
| 164 | + use indoc::indoc; |
| 165 | + |
| 166 | + const CASE: &str = indoc! {" |
| 167 | + RRRRIICCFF |
| 168 | + RRRRIICCCF |
| 169 | + VVRRRCCFFF |
| 170 | + VVRCCCJFFF |
| 171 | + VVVVCJJCFE |
| 172 | + VVIVCCJJEE |
| 173 | + VVIIICJJEE |
| 174 | + MIIIIIJJEE |
| 175 | + MIIISIJEEE |
| 176 | + MMMISSJEEE |
| 177 | + "}; |
| 178 | + |
| 179 | + #[test] |
| 180 | + fn part_a() { |
| 181 | + assert_eq!(super::part_a(CASE), 1930.into()); |
| 182 | + } |
| 183 | + |
| 184 | + #[test] |
| 185 | + fn part_b() { |
| 186 | + assert_eq!(super::part_b(CASE), 1206.into()); |
| 187 | + } |
| 188 | +} |
0 commit comments