Day 12: Garden Groups
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Rust
I essentially used flood fill to collect each region. Part 1 was then relatively easy: for each point, check how many neighbors are outside of the region.
Part 2 took me forever, and I ended up looking for hints online, where I discovered that an easy way to count the number of sides is to instead count the number of corners. Doing this for “normal” corners (e.g. in a square) was relatively easy, but “reverse corners” took me a long time. Corners like here what we see in the NE corner of the first
C
in the third row here:I’m more or less happy with my solution, but my brain is now totally fried.
https://gitlab.com/bricka/advent-of-code-2024-rust/-/blob/main/src/days/day12.rs?ref_type=heads
use std::collections::HashSet; use crate::grid::{Coordinate, Direction, Grid}; use crate::solver::DaySolver; fn perimeter_score(c: Coordinate, grid: &MyGrid) -> usize { let plant_type = grid[c]; Direction::orthogonal_iter() .map(|d| grid.neighbor_in_direction(c, d)) .map(|c_opt| match c_opt { None => 1, Some(c) => if grid[c] == plant_type { 0 } else { 1 } }) .sum() } type MyGrid = Grid<char>; struct Region { #[allow(dead_code)] plant_type: char, coordinates: HashSet<Coordinate>, } impl Region { fn new(plant_type: char, coordinates: HashSet<Coordinate>) -> Region { Region { plant_type, coordinates } } fn iter(&self) -> impl Iterator<Item = &Coordinate> { self.coordinates.iter() } fn part1_score(&self, grid: &MyGrid) -> usize { self.coordinates.len() * self.coordinates.iter().map(|c| perimeter_score(*c, grid)).sum::<usize>() } fn part2_score(&self, grid: &MyGrid) -> usize { let area = self.coordinates.len(); let sides = self.number_of_corners(grid); area * sides } fn number_of_corners(&self, grid: &MyGrid) -> usize { self.coordinates.iter().cloned() .map(|coordinate| { // How many corners do we have from here? // println!("Checking {}", border_coordinate); let corner_count = Direction::diagonal_iter() .filter(|corner_direction| { // Either: // 1) Both neighbor directions are not 100% in the region // 2) Both neighbors are in the region, but the corner itself isn't let corner_in_region = match grid.neighbor_in_direction(coordinate, *corner_direction) { None => false, Some(c) => self.coordinates.contains(&c), }; let both_neighbors_not_in_region = corner_direction.neighbor_directions().iter() .all(|direction| match grid.neighbor_in_direction(coordinate, *direction) { None => true, Some(c) => !self.coordinates.contains(&c), }); let both_neighbors_in_region = corner_direction.neighbor_directions().iter() .all(|direction| match grid.neighbor_in_direction(coordinate, *direction) { None => false, Some(c) => self.coordinates.contains(&c), }); both_neighbors_not_in_region || (both_neighbors_in_region && !corner_in_region) }) .count(); // println!("Corner count = {}", corner_count); corner_count }) .sum() } } fn parse_input(input: String) -> MyGrid { input.lines() .map(|line| line.chars().collect()) .collect::<Vec<Vec<char>>>() .into() } fn find_region_at(grid: &MyGrid, start: Coordinate) -> Region { let plant_type = grid[start]; let mut coordinates = HashSet::new(); let mut frontier = vec![start]; while let Some(coordinate) = frontier.pop() { if grid[coordinate] == plant_type && !coordinates.contains(&coordinate) { coordinates.insert(coordinate); frontier.extend(grid.orthogonal_neighbors_iter(coordinate)); } } Region::new(plant_type, coordinates) } fn find_regions(grid: &MyGrid) -> Vec<Region> { let mut visited_coordinates: HashSet<Coordinate> = HashSet::new(); let mut regions = vec![]; for coordinate in grid.coordinates_iter() { if !visited_coordinates.contains(&coordinate) { let region = find_region_at(grid, coordinate); visited_coordinates.extend(region.iter().cloned()); regions.push(region); } } regions } pub struct Day12Solver; impl DaySolver for Day12Solver { fn part1(&self, input: String) -> usize { let grid = parse_input(input); let regions = find_regions(&grid); regions.into_iter() .map(|region| region.part1_score(&grid)) .sum() } fn part2(&self, input: String) -> usize { let grid = parse_input(input); let regions = find_regions(&grid); regions.into_iter() .map(|region| region.part2_score(&grid)) .sum() } }
Counting the number of corners was a very useful hint for part 2. I had the most trouble with detecting the double corners, i.e. like in the example where the two B fields touch diagonally:
Still, I would’ve taken a lot longer and probably made really-bad-performance-code without reading this :D