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
Uiua
Takes about 3 seconds to solve both parts for live data, caused primarily by my terrible fill function in FieldCoords
which repeatedly refills and dedups already discovered cells. I promised myself when I wrote it that I would revisit it, but I really can’t be bothered right now. Sorry Kai.
LATE EDIT: Thanks to Quant for the inspiration to revisit this. With his code snippet and the realisation that I should normalise all fields to remove wasted space, runtime is now down to 55ms.
Data ← ⊜∘⊸≠@\n "AAAA\nBBCD\nBBCC\nEEEC"
N₄ ← [¯1_0 1_0 0_¯1 0_1] # Four orthogonal neighbours.
Fences ← /+/+=0≡(⬚0⊡+N₄¤)⊙¤⊚.°⊚ # Fences for a field, by looking for edges.
Cs ← [0 1 1 0 1 0 2 1 1 2 0 1 0 1 1 0] # Number of corners keyed by bitarray of 2x2 grid.
Sides ← /+♭⬚0⧈(⊡:Cs°⋯♭)2_2°⊚ # Add border, look for corners in 2x2 windows.
# Use `classify` to find fields, then normalise to 0_0.
Fields ← ≡⍚(-¤⊸/↧)⊜□:⇡△.+1⍜♭⊛Data # Thanks to Quant!
/+×≡◇⊃⧻Fences Fields
/+×≡◇⊃⧻Sides Fields
I found multidimensional markers for partition to work really well for finding the fields: Areas ← ⊜□:⇡△.+1⍜♭⊛
It just groups the other array’s contents according to adjacent markers, horizontally and vertically. Took me quite a bit to figure out what’s actually happening in the example in the documentation ^^’
Ooh, interesting, I’ll have to give that a try. Thanks!
(edit) Wow, that replaced my three lines of overly complex code without a hitch. classify
is an operator I never really got the point of before. Beautiful.
Data ← ⊜∘⊸≠@\n "AAAA\nBBCD\nBBCC\nEEEC"
N₄ ← [¯1_0 1_0 0_¯1 0_1] # Four orthogonal neighbours.
Fences ← /+≡(/+=0⬚0⊡+N₄¤)⊙¤⊚.°⊚ # Fences for a field, by looking for edges.
Cs ← [0 1 1 0 1 0 2 1 1 2 0 1 0 1 1 0] # Number of corners keyed by bitarray of 2x2 grid.
Sides ← /+/+⧈(⊡:Cs°⋯♭)2_2⌝↘¯1_¯1⌝↘1_1°⊚ # Add border, look for corners in 2x2 windows.
Fields ← ⊜□:⇡△.+1⍜♭⊛Data
/+×≡◇⊃⧻Fences Fields
/+×≡◇⊃⧻Sides Fields
C
No big trouble today, just a bit of careful debugging of my part 2 logic for which I was greatly helped by some Redditor’s testcase 🙏
Happy to have gotten it all in the single flood fill function without any extra passes.
Code
#include "common.h"
#define GZ 144
static char g[GZ][GZ];
static char seen[GZ][GZ];
static void
count(char c, int x, int y, int *area, int *perim, int *sides)
{
if (g[y][x] != c) { (*perim)++; return; }
if (seen[y][x]) return;
*area += 1;
seen[y][x] = 1;
/* count start of top/left edges, end of bottom/right edges */
*sides += g[y-1][x]!=c && ((g[y-1][x-1]==c) || (g[y][x-1]!=c));
*sides += g[y+1][x]!=c && ((g[y+1][x+1]==c) || (g[y][x+1]!=c));
*sides += g[y][x-1]!=c && ((g[y-1][x-1]==c) || (g[y-1][x]!=c));
*sides += g[y][x+1]!=c && ((g[y+1][x+1]==c) || (g[y+1][x]!=c));
count(c, x, y-1, area, perim, sides);
count(c, x, y+1, area, perim, sides);
count(c, x-1, y, area, perim, sides);
count(c, x+1, y, area, perim, sides);
}
int
main(int argc, char **argv)
{
int p1=0,p2=0, x,y, area, perim, sides;
if (argc > 1)
DISCARD(freopen(argv[1], "r", stdin));
for (y=1; fgets(g[y]+1, GZ-2, stdin); y++)
assert(y+1 < GZ);
for (y=1; y<GZ-1; y++)
for (x=1; x<GZ-1; x++)
if (isalpha(g[y][x]) && !seen[y][x]) {
area = perim = sides = 0;
count(g[y][x], x, y, &area, &perim, &sides);
p1 += area * perim;
p2 += area * sides;
}
printf("12: %d %d\n", p1, p2);
}
C#
public class Day12 : Solver
{
private string[] data;
private int width, height;
private Dictionary<int, long> perimeters = [];
private Dictionary<int, long> areas = [];
private Dictionary<int, long> sides = [];
private int region_count;
public void Presolve(string input) {
data = input.Trim().Split("\n").ToArray();
height = data.Length;
width = data[0].Length;
var graph_cc = MakeGraph(false);
var cc = new ConnectedComponentsAlgorithm<Point, PointEdge>(graph_cc);
cc.Compute();
var graph_all = MakeGraph(true);
Dictionary<(int Component, int Y), List<int>> x_sides = [];
Dictionary<(int Component, int X), List<int>> y_sides = [];
var search = new UndirectedBreadthFirstSearchAlgorithm<Point, PointEdge>(graph_all);
search.SetRootVertex((0, 0));
search.FinishVertex += vertex => {
if (IsWithinBounds(vertex.Item1, vertex.Item2)) {
int component = cc.Components[vertex];
areas.TryAdd(component, 0L);
areas[component] += 1;
}
};
search.ExamineEdge += edge => {
var (si, ti) = (IsWithinBounds(edge.Source), IsWithinBounds(edge.Target));
bool border = si != ti || cc.Components[edge.Source] != cc.Components[edge.Target];
if (si && border) {
int component = cc.Components[edge.Source];
perimeters.TryAdd(component, 0L);
perimeters[component] += 1;
if (edge.Source.Item1 == edge.Target.Item1) {
int y = Math.Min(edge.Source.Item2, edge.Target.Item2);
x_sides.TryAdd((component, y), []);
x_sides[(component, y)].Add(edge.Source.Item2 > edge.Target.Item2 ? edge.Source.Item1 : -edge.Source.Item1 - 5);
} else {
int x = Math.Min(edge.Source.Item1, edge.Target.Item1);
y_sides.TryAdd((component, x), []);
y_sides[(component, x)].Add(edge.Source.Item1 > edge.Target.Item1 ? edge.Source.Item2 : -edge.Source.Item2 - 5);
}
}
};
search.Compute();
region_count = cc.ComponentCount;
foreach (var side_projection in x_sides) {
side_projection.Value.Sort();
sides.TryAdd(side_projection.Key.Component, 0);
int last_x = int.MinValue;
foreach (var x in side_projection.Value) {
if (x != (last_x + 1)) sides[side_projection.Key.Component] += 1;
last_x = x;
}
}
foreach (var side_projection in y_sides) {
side_projection.Value.Sort();
sides.TryAdd(side_projection.Key.Component, 0);
int last_y = int.MinValue;
foreach (var y in side_projection.Value) {
if (y != (last_y + 1)) sides[side_projection.Key.Component] += 1;
last_y = y;
}
}
foreach (var component in Enumerable.Range(0, region_count)) {
if (!areas.ContainsKey(component)) continue;
}
}
public string SolveFirst() =>
Enumerable.Range(0, region_count)
.Where(component => areas.ContainsKey(component))
.Select(component => areas[component] * perimeters[component]).Sum().ToString();
public string SolveSecond() =>
Enumerable.Range(0, region_count)
.Where(component => areas.ContainsKey(component))
.Select(component => areas[component] * sides[component]).Sum().ToString();
private record struct PointEdge(Point Source, Point Target): IEdge<Point>;
private IUndirectedGraph<Point, PointEdge> MakeGraph(bool with_edges_between_plots)=>
new DelegateUndirectedGraph<Point, PointEdge>(GetVertices(), with_edges_between_plots? GetAllEdges : GetEdgesWithoutBorders, false);
private bool IsWithinBounds(int x, int y) => x >= 0 && x < width && y >= 0 && y < height;
private bool IsWithinBounds(Point p) => IsWithinBounds(p.Item1, p.Item2);
private readonly (int, int)[] directions = [(-1, 0), (0, -1), (1, 0), (0, 1)];
private bool GetEdgesWithoutBorders(Point arg, out IEnumerable<PointEdge> result) {
List<PointEdge> result_list = [];
var (x, y) = arg;
bool inside = IsWithinBounds(x, y);
foreach (var (dx, dy) in directions) {
var (ox, oy) = (x + dx, y + dy);
if (!inside || !IsWithinBounds(ox, oy)) continue;
if (data[y][x] == data[oy][ox]) result_list.Add(new(arg, (ox, oy)));
}
result = result_list;
return true;
}
private bool GetAllEdges(Point arg, out IEnumerable<PointEdge> result) {
List<PointEdge> result_list = [];
var (x, y) = arg;
foreach (var (dx, dy) in directions) {
var (ox, oy) = (x + dx, y + dy);
if (ox >= -1 && ox <= width && oy >= -1 && oy <= height) result_list.Add(new(arg, (ox, oy)));
}
result = result_list;
return true;
}
private IEnumerable<(int, int)> GetVertices() => Enumerable.Range(-1, width + 2).SelectMany(x => Enumerable.Range(-1, height + 2).Select(y => (x, y)));
}
Dart
Filling to find regions was easy. Counting areas was easy. Counting fences was okay. Counting sides caused me a lot of frustration as I tried and rejected a number of approaches, eventually arriving at a reasonably simple corner-counting approach. None of this was helped by all the examples lacking at least two important layouts, causing today to be the first day that I ran out of hints for wrong answers :-(.
(corners
is where the magic happens)
70 or so lines, half a second to run, so that's fine for today.
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:more/more.dart';
const List<Point> n4 = [Point(0, 1), Point(0, -1), Point(1, 0), Point(-1, 0)];
List<Point> n8 = n4 + [Point(1, 1), Point(1, -1), Point(-1, 1), Point(-1, -1)];
const List<Point> c4 = [Point(0, 0), Point(0, 1), Point(1, 0), Point(1, 1)];
(Map<Point, String>, Map<Point, List<Point>>) parse(ls) {
var nodes = {
for (var y in 0.to(ls.length))
for (var x in 0.to(ls.first.length)) Point<num>(x, y): ls[y][x] as String
};
var nexts = Map.fromEntries(nodes.keys.map((n) => MapEntry(
n,
n4
.map((d) => n + d)
.where((d) => (nodes[d] ?? '') == nodes[n]!)
.toList())));
return (nodes, nexts);
}
(int, Set<Point>) survey(
Point here, String target, Map<Point<num>, List<Point>> nexts,
[Set sofar = const {}]) {
seen.add(here);
var fences = 4 - nexts[here]!.length;
var area = {here};
for (var f in nexts[here]!.where((e) => !seen.contains(e))) {
var (fs, a) = survey(f, target, nexts, sofar.toSet()..add(f));
fences += fs;
area.addAll(a);
}
return (fences, area);
}
late Set<Point> seen;
List<(int, Set<Point<num>>)> costs(List<String> lines) {
seen = {};
var ret = <(int, Set<Point<num>>)>[];
var (nodes, nexts) = parse(lines);
var toVisit = nodes.keys.toSet();
while (toVisit.isNotEmpty) {
var here = toVisit.first;
toVisit.remove(here);
ret.add(survey(here, nodes[here]!, nexts));
toVisit.removeAll(seen);
}
return ret;
}
Function eq = const ListEquality().equals;
int corners(Set<Point> points) {
var border = points.map((e) => n8.map((n) => n + e)).flattenedToSet
..addAll(points);
// A corner is where a 2x2 grid contains one/three in-shape points, or
// two diagonally-opposite cells
var corners = 0;
for (var cell in border) {
var count = c4.map((e) => points.contains(e + cell)).toList();
if (count.count((e) => e) % 2 == 1) {
corners += 1;
} else {
if (eq(count, [true, false, false, true]) ||
eq(count, [false, true, true, false])) {
corners += 2;
}
}
}
return corners;
}
part1(lines) => costs(lines).map((e) => e.first * e.last.length).sum;
part2(lines) => costs(lines).map((e) => corners(e.last) * e.last.length).sum;
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:
....
..C.
..CC
...C
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:
AAAAAA
AAABBA
AAABBA
ABBAAA
ABBAAA
AAAAAA
Still, I would’ve taken a lot longer and probably made really-bad-performance-code without reading this :D