Skip to content

Commit 91f83eb

Browse files
committed
Faster simpler approach
1 parent 6eaba1c commit 91f83eb

File tree

2 files changed

+82
-85
lines changed

2 files changed

+82
-85
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
8383
| 9 | [Disk Fragmenter](https://adventofcode.com/2024/day/9) | [Source](src/year2024/day09.rs) | 106 |
8484
| 10 | [Hoof It](https://adventofcode.com/2024/day/10) | [Source](src/year2024/day10.rs) | 38 |
8585
| 11 | [Plutonian Pebbles](https://adventofcode.com/2024/day/11) | [Source](src/year2024/day11.rs) | 248 |
86-
| 12 | [Garden Groups](https://adventofcode.com/2024/day/12) | [Source](src/year2024/day12.rs) | 1582 |
86+
| 12 | [Garden Groups](https://adventofcode.com/2024/day/12) | [Source](src/year2024/day12.rs) | 310 |
8787

8888
## 2023
8989

src/year2024/day12.rs

Lines changed: 81 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,117 @@
11
//! # Garden Groups
2+
//!
3+
//! Solves both parts simultaneously by flood filling each region.
4+
//!
5+
//! For part one we increment the perimeter for each neighbouring plot out of bounds
6+
//! or a different kind of plant.
7+
//!
8+
//! For part two we count corners, as the number of corners equals the number of sides.
9+
//! We remove a corner when another plot is adjacent either up, down, left or right.
10+
//! For example, considering the top left corner of the plot:
11+
//!
12+
//! ```none
13+
//! .. .# .. ##
14+
//! .# ✓ .# ✗ ## ✗ ## ✗
15+
//! ```
16+
//!
17+
//! However we add back a corner when it's concave, for example where a plot is above, left but
18+
//! not above and to the left:
19+
//!
20+
//! ```none
21+
//! .#
22+
//! ## ✓
23+
//! ```
24+
//!
25+
//! There are 8 neighbours to check, giving 2⁸ possibilities. To speed things up these are
26+
//! precomputed and cached in a lookup table.
227
use crate::util::grid::*;
3-
use crate::util::hash::*;
428
use crate::util::point::*;
5-
use std::collections::VecDeque;
29+
use std::array::from_fn;
630

7-
const CLOCKWISE: [Point; 5] = [UP, RIGHT, DOWN, LEFT, UP];
31+
type Input = (usize, usize);
832

9-
pub fn parse(input: &str) -> Grid<u8> {
10-
Grid::parse(input)
11-
}
33+
pub fn parse(input: &str) -> Input {
34+
let grid = Grid::parse(input);
35+
let lut = sides_lut();
36+
37+
let mut region = Vec::new();
38+
let mut seen = grid.same_size_with(0);
1239

13-
pub fn part1(grid: &Grid<u8>) -> i32 {
14-
let mut todo = VecDeque::new();
15-
let mut seen = grid.same_size_with(false);
16-
let mut added = grid.same_size_with(false);
17-
let mut result = 0;
40+
let mut part_one = 0;
41+
let mut part_two = 0;
1842

1943
for y in 0..grid.height {
2044
for x in 0..grid.width {
45+
// Skip already filled points.
2146
let point = Point::new(x, y);
22-
if seen[point] {
47+
if seen[point] > 0 {
2348
continue;
2449
}
2550

51+
// Assign a unique id to each region based on the first point that we encounter.
2652
let kind = grid[point];
53+
let id = y * grid.width + x + 1;
54+
55+
// Flood fill, using area as an index.
2756
let mut area = 0;
28-
let mut perm = 0;
57+
let mut perimeter = 0;
58+
let mut sides = 0;
2959

30-
todo.push_back(point);
31-
seen[point] = true;
60+
region.push(point);
61+
seen[point] = id;
3262

33-
while let Some(point) = todo.pop_front() {
63+
while area < region.len() {
64+
let point = region[area];
3465
area += 1;
35-
perm += 4;
36-
added[point] = true;
3766

3867
for next in ORTHOGONAL.map(|o| point + o) {
3968
if grid.contains(next) && grid[next] == kind {
40-
if !seen[next] {
41-
seen[next] = true;
42-
todo.push_back(next);
43-
}
44-
if added[next] {
45-
perm -= 2;
69+
if seen[next] == 0 {
70+
region.push(next);
71+
seen[next] = id;
4672
}
73+
} else {
74+
perimeter += 1;
4775
}
4876
}
4977
}
5078

51-
result += area * perm;
79+
// Sum sides for all plots in the region.
80+
for point in region.drain(..) {
81+
let index = DIAGONAL.iter().fold(0, |acc, &d| {
82+
(acc << 1) | (seen.contains(point + d) && seen[point + d] == id) as usize
83+
});
84+
sides += lut[index];
85+
}
86+
87+
part_one += area * perimeter;
88+
part_two += area * sides;
5289
}
5390
}
5491

55-
result
92+
(part_one, part_two)
5693
}
5794

58-
pub fn part2(grid: &Grid<u8>) -> u32 {
59-
let mut seen = grid.same_size_with(false);
60-
let mut todo = VecDeque::new();
61-
let mut corner = FastMap::new();
62-
let mut middle = FastMap::new();
63-
let mut result = 0;
64-
65-
for y in 0..grid.height {
66-
for x in 0..grid.width {
67-
let point = Point::new(x, y);
68-
if seen[point] {
69-
continue;
70-
}
71-
72-
let kind = grid[point];
73-
let mut size = 0;
74-
let mut sides = 0;
75-
76-
todo.push_back(point);
77-
seen[point] = true;
78-
79-
while let Some(point) = todo.pop_front() {
80-
size += 1;
81-
let x = 2 * point.x;
82-
let y = 2 * point.y;
83-
84-
*corner.entry(Point::new(x, y)).or_insert(0) += 1;
85-
*corner.entry(Point::new(x + 2, y)).or_insert(0) += 1;
86-
*corner.entry(Point::new(x, y + 2)).or_insert(0) += 1;
87-
*corner.entry(Point::new(x + 2, y + 2)).or_insert(0) += 1;
88-
89-
*middle.entry(Point::new(x + 1, y)).or_insert(0) += 1;
90-
*middle.entry(Point::new(x, y + 1)).or_insert(0) += 1;
91-
*middle.entry(Point::new(x + 2, y + 1)).or_insert(0) += 1;
92-
*middle.entry(Point::new(x + 1, y + 2)).or_insert(0) += 1;
93-
94-
for next in ORTHOGONAL.map(|o| point + o) {
95-
if grid.contains(next) && grid[next] == kind && !seen[next] {
96-
seen[next] = true;
97-
todo.push_back(next);
98-
}
99-
}
100-
}
95+
pub fn part1(input: &Input) -> usize {
96+
input.0
97+
}
10198

102-
for (&point, _) in corner.iter().filter(|(_, &v)| v < 4) {
103-
let freq = CLOCKWISE.map(|c| *middle.get(&(point + c)).unwrap_or(&2));
104-
let count = freq.windows(2).filter(|w| w[0] < 2 && w[1] < 2).count();
99+
pub fn part2(input: &Input) -> usize {
100+
input.1
101+
}
105102

106-
if count == 1 {
107-
sides += 1;
108-
} else if count == 4 {
109-
sides += 2;
110-
}
111-
}
103+
/// There are 8 neighbours to check, giving 2⁸ possibilities.
104+
/// Precompute the number of corners once into a lookup table to speed things up.
105+
fn sides_lut() -> [usize; 256] {
106+
from_fn(|neighbours| {
107+
let [up_left, up, up_right, left, right, down_left, down, down_right] =
108+
from_fn(|i| neighbours & (1 << i) > 0);
112109

113-
corner.clear();
114-
middle.clear();
115-
result += size * sides;
116-
}
117-
}
110+
let ul = !(up || left) || (up && left && !up_left);
111+
let ur = !(up || right) || (up && right && !up_right);
112+
let dl = !(down || left) || (down && left && !down_left);
113+
let dr = !(down || right) || (down && right && !down_right);
118114

119-
result
115+
(ul as usize) + (ur as usize) + (dl as usize) + (dr as usize)
116+
})
120117
}

0 commit comments

Comments
 (0)