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
Haskell
This was a bit of a fiddly one. There’s probably scope for golfing it down some more, but I’ve had enough for today :3
Solution
import Control.Arrow
import Data.List
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
readInput :: String -> Map (Int, Int) Char
readInput s = Map.fromList [((i, j), c) | (i, l) <- zip [0 ..] (lines s), (j, c) <- zip [0 ..] l]
(i1, j1) .+. (i2, j2) = (i1 + i2, j1 + j2)
(i1, j1) .-. (i2, j2) = (i1 - i2, j1 - j2)
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] :: [(Int, Int)]
edges = zip ps (drop 1 ps) :: [((Int, Int), (Int, Int))]
where
ps = [(0, 1), (1, 1), (1, 0), (0, 0), (0, 1)]
regions :: Map (Int, Int) Char -> [Set (Int, Int)]
regions = unfoldr (fmap (uncurry removeRegion) . Map.minViewWithKey)
where
removeRegion (p, t) = go Set.empty (Set.singleton p)
where
go r ps plots
| Set.null ps = (r, plots)
| otherwise =
let ps' =
Set.filter (\p -> plots Map.!? p == Just t) $
Set.fromList (concatMap adjacent ps) Set.\\ ps
in go (Set.union r ps) ps' (Map.withoutKeys plots ps')
adjacent = (`map` directions) . (.+.)
boundary :: Set (Int, Int) -> Set ((Int, Int), (Int, Int))
boundary region =
Set.fromList $
[ (p .+. e1, p .+. e2)
| p <- Set.elems region,
(d, (e1, e2)) <- zip directions edges,
p .+. d `Set.notMember` region
]
perimeter :: Set (Int, Int) -> [[(Int, Int)]]
perimeter = unfoldr (fmap (uncurry removeChain) . Set.minView) . boundary
where
removeChain e@(e1, e2) es = first (e1 :) $ go [] e es
go c e@(e1, e2) es =
case find ((== e2) . fst) es of
Nothing -> (e1 : c, es)
Just e' -> go (e1 : c) e' (Set.delete e' es)
countSides :: [(Int, Int)] -> Int
countSides ps = length $ group $ zipWith (.-.) (drop 1 ps) ps
main = do
input <- readInput <$> readFile "input12"
let rs = map (Set.size &&& perimeter) $ regions input
print . sum $ map (\(a, p) -> a * sum (map (subtract 1 . length) p)) rs
print . sum $ map (\(a, p) -> a * sum (map countSides p)) rs
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;
Ended up oversleeping somewhat, so I did the first part on the way to work using flood fills over a global visited set, and now that work’s over I’ve sat down to expand that solution to do corner counting for part two as well.
C#
char[] map = new char[0];
(int X, int Y) size = (0, 0);
public void Input(IEnumerable<string> lines)
{
map = string.Concat(lines).ToCharArray();
size = (lines.First().Length, lines.Count());
}
Dictionary<HashSet<(int,int)>,int> areas = new Dictionary<HashSet<(int,int)>,int>();
public void PreCalc()
{
HashSet<(int, int)> visited = new HashSet<(int, int)>();
for (int y = 0; y < size.Y; ++y)
for (int x = 0; x < size.X; ++x)
{
var at = (x, y);
if (visited.Contains(at))
continue;
var area = Flood((x, y), visited);
areas[area.Area] = area.Perim;
}
}
public void Part1()
{
int sum = areas.Select(kv => kv.Key.Count * kv.Value).Sum();
Console.WriteLine($"Fencing total: {sum}");
}
public void Part2()
{
int sum = areas.Select(kv => kv.Key.Count * countCorners(kv.Key)).Sum();
Console.WriteLine($"Fencing total: {sum}");
}
readonly (int dX, int dY)[] links = new[] { (1, 0), (0, 1), (-1, 0), (0, -1) };
(HashSet<(int,int)> Area, int Perim) Flood((int X, int Y) from, HashSet<(int X, int Y)> visited)
{
char at = map[from.Y * size.X + from.X];
(HashSet<(int,int)> Area, int Perim) ret = (new HashSet<(int,int)>(), 0);
visited.Add(from);
ret.Area.Add(from);
foreach (var link in links)
{
(int X, int Y) newAt = (from.X + link.dX, from.Y + link.dY);
char offset ;
if (newAt.X < 0 || newAt.X >= size.X || newAt.Y < 0 || newAt.Y >= size.Y)
offset = '\0';
else
offset = map[newAt.Y * size.X + newAt.X];
if (offset == at)
{
if (visited.Contains(newAt))
continue;
var nextArea = Flood(newAt, visited);
ret.Area.UnionWith(nextArea.Area);
ret.Perim += nextArea.Perim;
}
else
{
ret.Perim += 1;
}
}
return ret;
}
readonly (int dX, int dY)[] cornerPoints = new[] { (0, 0), (1, 0), (1, 1), (0, 1) };
readonly int[] diagonalValues = new[] { (2 << 0) + (2 << 2), (2 << 1) + (2 << 3) };
int countCorners(HashSet<(int X, int Y)> points)
{
int corners = 0;
var bounds = findBounds(points);
for (int y = bounds.minY - 1; y < bounds.maxY + 1; ++y)
for (int x = bounds.minX - 1; x < bounds.maxX + 1; ++x)
{
var atPoint = cornerPoints.Select(c => points.Contains((x + c.dX, y + c.dY)));
var before = corners;
if (atPoint.Where(c => c).Count() % 2 == 1)
corners++;
else if (diagonalValues.Contains(atPoint.Select((c, i) => c ? (2 << i) : 0).Sum()))
corners += 2;
}
return corners;
}
(int minX, int minY, int maxX, int maxY) findBounds(HashSet<(int X, int Y)> points)
{
(int minX, int minY, int maxX, int maxY) ret = (int.MaxValue, int.MaxValue, int.MinValue, int.MinValue);
foreach (var point in points)
{
ret.minX = Math.Min(ret.minX, point.X);
ret.minY = Math.Min(ret.minY, point.Y);
ret.maxX = Math.Max(ret.maxX, point.X);
ret.maxY = Math.Max(ret.maxY, point.Y);
}
return ret;
}