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
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
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
Part 1: I use flood fill to count all grouped plants and keep track of each border I see.
Part 2: I use an algorithm similar to "merge overlapping ranges" to count spans of borders (border orientation matters) in each row and column, for each group. Resulting code (hidden under spoiler) is a little messy and not very DRY (it's completely soaked).
Edit: refactored solution, removed some very stupid code.
`proc groupSpans()`
proc groupSpans(borders: seq[(Vec2, Dir)]): int =
## returns number of continuous groups of cells with same Direction
## and on the same row or column
var borders = borders
var horiz = borders.filterIt(it[1] in {U, D})
while horiz.len > 0:
var sameYandDir = @[horiz.pop()]
var curY = sameYandDir[^1][0].y
var curDir = sameYandDir[^1][1]
for i in countDown(horiz.high, 0):
if horiz[i][0].y == curY and horiz[i][1] == curDir:
sameYandDir.add horiz[i]
horiz.del i
sameYandDir.sort((a,b)=>cmp(a[0].x, b[0].x), Descending)
var cnt = 1
for i, (p,d) in sameYandDir.toOpenArray(1, sameYandDir.high):
if sameYandDir[i][0].x - p.x != 1: inc cnt
result += cnt
var vert = borders.filterIt(it[1] in {L, R})
while vert.len > 0:
var sameXandDir = @[vert.pop()]
var curX = sameXandDir[^1][0].x
var curDir = sameXandDir[^1][1]
for i in countDown(vert.high, 0):
if vert[i][0].x == curX and vert[i][1] == curDir:
sameXandDir.add vert[i]
vert.del i
sameXandDir.sort((a,b)=>cmp(a[0].y, b[0].y), Descending)
var cnt = 1
for i, (p,d) in sameXandDir.toOpenArray(1, sameXandDir.high):
if sameXandDir[i][0].y - p.y != 1: inc cnt
result += cnt
type
Dir = enum L,R,U,D
Vec2 = tuple[x,y: int]
GroupData = object
plantCount: int
borders: seq[(Vec2, Dir)]
const Adjacent: array[4, Vec2] = [(-1,0),(1,0),(0,-1),(0,1)]
proc solve(input: string): AOCSolution[int, int] =
let grid = input.splitLines()
var visited = newSeqWith(grid.len, newSeq[bool](grid[0].len))
var groups: seq[GroupData]
proc floodFill(pos: Vec2, plant: char, groupId: int) =
visited[pos.y][pos.x] = true
inc groups[groupId].plantCount
for di, d in Adjacent:
let pd: Vec2 = (pos.x+d.x, pos.y+d.y)
if pd.x < 0 or pd.y < 0 or pd.x > grid[0].high or pd.y > grid.high or
grid[pd.y][pd.x] != plant:
groups[groupId].borders.add (pd, Dir(di))
continue
if visited[pd.y][pd.x]: continue
floodFill(pd, plant, groupId)
for y in 0..grid.high:
for x in 0..grid[0].high:
if visited[y][x]: continue
groups.add GroupData()
floodFill((x,y), grid[y][x], groups.high)
for gid, group in groups:
result.part1 += group.plantCount * group.borders.len
result.part2 += group.plantCount * group.borders.groupSpans()
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.
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
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;
Detecting regions is a floodfill.
For Part 2, I select all adjacent tiles that are not part of a region and group them by the direction relative to the closest region tile, then group adjacent tiles with the same direction again and count.
Edit:
Takes 0.06s
Reveal Code
import Control.Arrow
import Data.Array.Unboxed (UArray)
import Data.Set (Set)
import Data.Map (Map)
import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Array.Unboxed as UArray
parse :: String -> UArray (Int, Int) Char
parse s = UArray.listArray ((1, 1), (n, m)) . filter (/= '\n') $ s
where
n = takeWhile (/= '\n') >>> length $ s
m = filter (== '\n') >>> length >>> pred $ s
neighborCoordinates (p1, p2) = [(p1-1, p2), (p1, p2-1), (p1, p2+1), (p1+1, p2)]
allNeighbors p a = neighborCoordinates
>>> filter (UArray.inRange (UArray.bounds a))
$ p
regionNeighbors p a = allNeighbors p
>>> filter ((a UArray.!) >>> (== pTile))
$ a
where
pTile = a UArray.! p
floodArea :: Set (Int, Int) -> Set (Int, Int) -> UArray (Int, Int) Char -> Set (Int, Int)
floodArea e o a
| Set.null o = e
| otherwise = floodArea e' o' a
where
e' = Set.union e o
o' = Set.fold (Set.union . Set.fromDistinctAscList . (filter (`Set.notMember` e')) . (flip regionNeighbors a)) Set.empty o
findRegions garden = findRegions' (Set.fromList . UArray.indices $ garden) garden
findRegions' remainingIndices garden
| Set.null remainingIndices = []
| otherwise = removedIndices : findRegions' remainingIndices' garden
where
removedIndices = floodArea Set.empty (Set.singleton . Set.findMin $ remainingIndices) garden
remainingIndices' = Set.difference remainingIndices removedIndices
perimeter region = Set.fold ((+) . length . filter (`Set.notMember` region) . neighborCoordinates) 0 region
part1 rs = map (Set.size &&& perimeter)
>>> map (uncurry (*))
>>> sum
$ rs
turnLeft ( 0, 1) = (-1, 0) -- right
turnLeft ( 0,-1) = ( 1, 0) -- left
turnLeft ( 1, 0) = ( 0, 1) -- down
turnLeft (-1, 0) = ( 0,-1) -- up
turnRight = turnLeft . turnLeft . turnLeft
move (py, px) (dy, dx) = (py + dy, px + dx)
tupleDelta (y1, x1) (y2, x2) = (y1-y2, x1-x2)
isRegionInner region p = all (`Set.member` region) (neighborCoordinates p)
groupEdges d ps
| Set.null ps = []
| otherwise = collectedEdge : groupEdges d ps'
where
ps' = Set.difference ps collectedEdge
collectedEdge = Set.union leftPoints rightPoints
leftPoints = iterate (move dl)
>>> takeWhile (`Set.member` ps)
>>> Set.fromList
$ currentPoint
rightPoints = iterate (move dr)
>>> takeWhile (`Set.member` ps)
>>> Set.fromList
$ currentPoint
currentPoint = Set.findMin ps
dr = turnRight d
dl = turnLeft d
linearPerimeter region = Map.foldr ((+) . length) 0 $ groupedEdges
where
edgeTiles = Set.filter (not . isRegionInner region) region
regionNeighbors = List.concatMap (\ p -> map (p,). filter (`Set.notMember` region) . neighborCoordinates $ p) . Set.toList $ region
groupedNeighbors = List.map (uncurry tupleDelta &&& Set.singleton . snd)
>>> Map.fromListWith (Set.union)
$ regionNeighbors
groupedEdges = Map.mapWithKey groupEdges
$ groupedNeighbors
part2 rs = map (Set.size &&& linearPerimeter)
>>> map (uncurry (*))
>>> sum
$ rs
main = getContents
>>= print
. (part1 &&& part2)
. findRegions
. parse
Areas are found by flooding, in the meantime whenever the adjacent plot would be outside the region (or out of bounds) the edge (inside plot, outside plot) is saved in a perimeter list. Part 1 takes just the size of that list, in part 2 we remove fence parts and all entries directly next to it on both sides.
Solution
use std::collections::{HashSet, VecDeque};
use euclid::{default::*, point2, vec2};
type Fences = HashSet<(Point2D<i32>, Point2D<i32>)>;
const DIRS: [Vector2D<i32>; 4] = [vec2(0, -1), vec2(1, 0), vec2(0, 1), vec2(-1, 0)];
fn parse(input: &str) -> Vec<&[u8]> {
input.lines().map(|l| l.as_bytes()).collect()
}
fn price(field: &[&[u8]], start: (usize, usize), visited: &mut [Vec<bool>]) -> (u32, Fences) {
let crop = field[start.1][start.0];
let width = field[0].len();
let height = field.len();
let mut area_visited = vec![vec![false; width]; height];
let mut area = 0;
let mut fences: Fences = HashSet::new();
area_visited[start.1][start.0] = true;
visited[start.1][start.0] = true;
let start = point2(start.0 as i32, start.1 as i32);
let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height).to_i32());
let mut frontier = VecDeque::from([start]);
while let Some(p) = frontier.pop_front() {
area += 1;
for dir in DIRS {
let next = p + dir;
if bounds.contains(next) {
let next_u = next.to_usize();
if area_visited[next_u.y][next_u.x] {
continue;
}
if field[next_u.y][next_u.x] == crop {
visited[next_u.y][next_u.x] = true;
area_visited[next_u.y][next_u.x] = true;
frontier.push_back(next);
continue;
}
}
fences.insert((p, next));
}
}
(area, fences)
}
fn part1(input: String) {
let field = parse(&input);
let width = field[0].len();
let height = field.len();
let mut visited = vec![vec![false; width]; height];
let mut total_price = 0;
for y in 0..height {
for x in 0..width {
if !visited[y][x] {
let (area, fences) = price(&field, (x, y), &mut visited);
total_price += area * fences.len() as u32;
}
}
}
println!("{total_price}");
}
fn count_perimeter(mut fences: Fences) -> u32 {
let list: Vec<_> = fences.iter().copied().collect();
let mut perimeter = 0;
for (v, w) in list {
if fences.contains(&(v, w)) {
perimeter += 1;
let dir = w - v;
let orth = dir.yx();
let mut next = v + orth;
while fences.remove(&(next, next + dir)) {
next += orth;
}
let mut next = v - orth;
while fences.remove(&(next, next + dir)) {
next -= orth;
}
}
}
perimeter
}
fn part2(input: String) {
let field = parse(&input);
let width = field[0].len();
let height = field.len();
let mut visited = vec![vec![false; width]; height];
let mut total_price = 0;
for y in 0..height {
for x in 0..width {
if !visited[y][x] {
let (area, fences) = price(&field, (x, y), &mut visited);
total_price += area * count_perimeter(fences);
}
}
}
println!("{total_price}");
}
util::aoc_main!();
Had to rely on an external polygon library for this one. Part 1 could have been easily done without it but part 2 would be diffucult (you can even use the simplify function to count the number of straight edges in internal and external boundaries modulo checking the collinearity of the start and end of the boundary)
import numpy as np
from pathlib import Path
from shapely import box, union, MultiPolygon, Polygon, MultiLineString
cwd = Path(__file__).parent
def parse_input(file_path):
with file_path.open("r") as fp:
garden = list(map(list, fp.read().splitlines()))
return np.array(garden)
def get_polygon(plant, garden):
coords = list(map(tuple, list(np.argwhere(garden==plant))))
for indc,coord in enumerate(coords):
box_next = box(xmin=coord[0], ymin=coord[1], xmax=coord[0]+1,
ymax=coord[1]+1)
if indc==0:
poly = box_next
else:
poly = union(poly, box_next)
if isinstance(poly, Polygon):
poly = MultiPolygon([poly])
return poly
def are_collinear(coords, tol=None):
coords = np.array(coords, dtype=float)
coords -= coords[0]
return np.linalg.matrix_rank(coords, tol=tol)==1
def simplify_boundary(boundary):
# if the object has internal and external boundaries then split them
# and recurse
if isinstance(boundary, MultiLineString):
coordinates = []
for b in boundary.geoms:
coordinates.append(simplify_boundary(b))
return list(np.concat(coordinates, axis=0))
simple_boundary = boundary.simplify(0)
coords = [np.array(x) for x in list(simple_boundary.coords)[:-1]]
resolved = False
while not resolved:
end_side=\
np.concat([x[:,None] for x in [coords[-1], coords[0], coords[1]]], axis=1)
if are_collinear(end_side.T):
coords = coords[1:]
else:
resolved = True
return coords
def solve_problem(file_name):
garden = parse_input(Path(cwd, file_name))
unique_plants = set(garden.flatten())
total_price = 0
discounted_total_price = 0
for plant in unique_plants:
polygon = get_polygon(plant, garden)
for geom in polygon.geoms:
coordinates = simplify_boundary(geom.boundary)
total_price += geom.area*geom.length
discounted_total_price += geom.area*len(coordinates)
return int(total_price), int(discounted_total_price)
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;
}
There is probably a more efficient way of finding the sides, but this way was what came to me.
using System.Diagnostics;
using Common;
namespace Day12;
static class Program
{
static void Main()
{
var start = Stopwatch.GetTimestamp();
var sampleInput = Input.ParseInput("sample.txt");
var programInput = Input.ParseInput("input.txt");
var (samplePart1, samplePart2) = Solve(sampleInput);
Console.WriteLine($"Part 1 sample: {samplePart1}");
Console.WriteLine($"Part 1 input: {samplePart2}");
var (inputPart1, inputPart2) = Solve(programInput);
Console.WriteLine($"Part 2 sample: {inputPart1}");
Console.WriteLine($"Part 2 input: {inputPart2}");
Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
}
static (int part1, int part2) Solve(Input i)
{
var retail = 0;
var bulk = 0;
var allPlotPoints = new Dictionary<char, HashSet<Point>>();
foreach (var p in Grid.EnumerateAllPoints(i.Bounds))
{
var plant = i.ElementAt(p);
if (!allPlotPoints.TryGetValue(plant, out var previousPlotPoints))
{
previousPlotPoints = new();
allPlotPoints[plant] = previousPlotPoints;
}
else if (previousPlotPoints.Contains(p)) continue;
var plotPoints = new HashSet<Point>();
var perimeter = SearchPlot(i, plotPoints, plant, p);
var area = plotPoints.Count;
var sides = CountSides(plotPoints);
retail += area * perimeter;
bulk += area * sides;
previousPlotPoints.AddRange(plotPoints);
}
return (retail, bulk);
}
static int CountSides(HashSet<Point> plot)
{
var sides = 0;
// Track the points we've visited searching for sides
HashSet<Point> visitedDownRight = new(),
visitedDownLeft = new(),
visitedRightDown = new(),
visitedRightUp = new();
// Sort the points in the plot from upper-left to lower-right, so we can
// go through them in reading order
foreach (var p in plot.OrderBy(p => (p.Row * 10000) + p.Col))
{
// Move right while looking up
sides += GetSideLength(p, plot, visitedRightUp, Direction.Right, Direction.Up) > 0 ? 1 : 0;
// Move right while looking down
sides += GetSideLength(p, plot, visitedRightDown, Direction.Right, Direction.Down) > 0 ? 1 : 0;
// Move down while looking right
sides += GetSideLength(p, plot, visitedDownRight, Direction.Down, Direction.Right) > 0 ? 1 : 0;
// Move down while looking left
sides += GetSideLength(p, plot, visitedDownLeft, Direction.Down, Direction.Left) > 0 ? 1 : 0;
}
return sides;
}
static int GetSideLength(Point p, HashSet<Point> plotPoints, HashSet<Point> visited, Direction move, Direction look)
{
if (!plotPoints.Contains(p)) return 0;
if (!visited.Add(p)) return 0;
if (plotPoints.Contains(p.Move(look))) return 0;
return 1 + GetSideLength(p.Move(move), plotPoints, visited, move, look);
}
static int SearchPlot(Input i, HashSet<Point> plotPoints, char plant, Point p)
{
if (!plotPoints.Add(p)) return 0;
return p
.GetCardinalMoves()
.Select(move =>
{
if (!i.IsInBounds(move) || (i.ElementAt(move) != plant)) return 1;
return SearchPlot(i, plotPoints, plant, move);
})
.Sum();
}
}
public class Input
{
public required string[] Map { get; init; }
public Point Bounds => new Point(this.Map.Length, this.Map[0].Length);
public char ElementAt(Point p) => this.Map[p.Row][p.Col];
public bool IsInBounds(Point p) => p.IsInBounds(this.Map.Length, this.Map[0].Length);
public static Input ParseInput(string file) => new Input()
{
Map = File.ReadAllLines(file),
};
}
Part 1: Simple DFS counting up the cells and exposed edges
Part 2: Still DFS, however I chose to keep track of all segments of the area, merging them if two segments connected. In the end, number of non-overlapping, non-intersecting segments is equal to number of sides. Not the most efficient solution, but it works.
import os
from collections import defaultdict
# paths
here = os.path.dirname(os.path.abspath(__file__))
filepath = os.path.join(here, "input.txt")
# read input
with open(filepath, mode="r", encoding="utf8") as f:
data = f.read()
# setup input vars
garden = data.splitlines()
m, n = len(garden), len(garden[0])
def part1():
visited = set()
def calcFenceCostFrom(i, j):
"""Calculates the fencing cost of the region starting from coords (i, j)"""
global garden, m, n
plant_type = garden[i][j]
stack = [(i, j)]
area, perimeter = 0, 0
while stack:
ci, cj = stack.pop()
if (ci, cj) in visited:
continue
visited.add((ci, cj))
# add cell to area
area += 1
# check top cell
if ci > 0 and garden[ci - 1][cj] == plant_type:
stack.append((ci - 1, cj))
else:
# if no top cell, add the edge to perimeter
perimeter += 1
# check left cell
if cj > 0 and garden[ci][cj - 1] == plant_type:
stack.append((ci, cj - 1))
else:
# if no left cell, add the edge to perimeter
perimeter += 1
# check bottom cell
if ci < m - 1 and garden[ci + 1][cj] == plant_type:
stack.append((ci + 1, cj))
else:
# if no bottom cell, add the edge to perimeter
perimeter += 1
# check right cell
if cj < n - 1 and garden[ci][cj + 1] == plant_type:
stack.append((ci, cj + 1))
else:
# if no right cell, add the edge to perimeter
perimeter += 1
return area * perimeter
# calculate fencing cost for every region
fencing_cost = 0
for i in range(m):
for j in range(n):
if (i, j) in visited:
continue
fencing_cost += calcFenceCostFrom(i, j)
print(fencing_cost)
def part2():
visited = set()
def calcFenceCostFrom(i, j):
"""Calculates the fencing cost of the region starting from coords (i, j)"""
global garden, m, n
plant_type = garden[i][j]
stack = [(i, j)]
area = 0
# keep track of all distinct, non-intersecting horizontal and vertical segments
segments = {
"H": defaultdict(set),
"V": defaultdict(set)
} # fmt: skip
while stack:
ci, cj = stack.pop()
if (ci, cj) in visited:
continue
visited.add((ci, cj))
# add cell to area
area += 1
# check top cell
if ci > 0 and garden[ci - 1][cj] == plant_type:
stack.append((ci - 1, cj))
else:
# record edge segment
ei = ci - 0.25 # push out the horizontal segment
segment_set = segments["H"][ei]
ej_from, ej_to = cj - 0.5, cj + 0.5 # extend the segment to connect with neighbors
merge_segments(segment_set, ej_from, ej_to) # merge with current segment set
# check left cell
if cj > 0 and garden[ci][cj - 1] == plant_type:
stack.append((ci, cj - 1))
else:
# record edge segment
ej = cj - 0.25 # push out the vertical segment
segment_set = segments["V"][ej]
ei_from, ei_to = ci - 0.5, ci + 0.5 # extend the segment to connect with neighbors
merge_segments(segment_set, ei_from, ei_to) # merge with current segment set
# check bottom cell
if ci < m - 1 and garden[ci + 1][cj] == plant_type:
stack.append((ci + 1, cj))
else:
# record edge segment
ei = ci + 0.25 # push out the horizontal segment
segment_set = segments["H"][ei]
ej_from, ej_to = cj - 0.5, cj + 0.5 # extend the segment to connect with neighbors
merge_segments(segment_set, ej_from, ej_to) # merge with current segment set
# check right cell
if cj < n - 1 and garden[ci][cj + 1] == plant_type:
stack.append((ci, cj + 1))
else:
# record edge segment
ej = cj + 0.25 # push out the vertical segment
segment_set = segments["V"][ej]
ei_from, ei_to = ci - 0.5, ci + 0.5 # extend the segment to connect with neighbors
merge_segments(segment_set, ei_from, ei_to) # merge with current segment set
# number of distinct segments == number of sides
sides = sum(len(segment_set) for seg_dict in segments.values() for segment_set in seg_dict.values())
return area * sides
def merge_segments(segment_set: set, idx_from: int, idx_to: int):
"""Merge segment into existing segment set"""
# find any overlapping / intersecting segments before and after current
former_segment, latter_segment = None, None
for s_from, s_to in segment_set:
if s_from < idx_from and s_to >= idx_from:
former_segment = (s_from, s_to)
if s_to > idx_to and s_from <= idx_to:
latter_segment = (s_from, s_to)
if former_segment is None and latter_segment is None:
# there is no overlapping segment
segment_set.add((idx_from, idx_to))
elif former_segment == latter_segment:
# the overlap segment contains our full segment
pass
elif former_segment is None:
# there is a latter segment only
segment_set.remove(latter_segment)
segment_set.add((idx_from, latter_segment[1]))
elif latter_segment is None:
# there is a former segment only
segment_set.remove(former_segment)
segment_set.add((former_segment[0], idx_to))
else:
# both are disconnected segments
segment_set.remove(former_segment)
segment_set.remove(latter_segment)
segment_set.add((former_segment[0], latter_segment[1]))
fencing_cost = 0
for i in range(m):
for j in range(n):
if (i, j) in visited:
continue
fencing_cost += calcFenceCostFrom(i, j)
print(fencing_cost)
part1()
part2()
I spent a while thinking about how to best do a flood fill in Uiua when I saw that β (partition) works beautifully with multidimensional markers: "Groups are formed from markers that are adjacent along any axis.", meaning I just had to convert all letters into numbers and I'd get all indices belonging to a field into an array.
For part 2, I cheated a bit by coming here and reading that you only need to count the edges. To my surprise, the second part is actually a bit faster than part 1. Takes less than 0.2 seconds each though :D