Day 4: Printing Department
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
Suspiciously easy. I even included a free animation generator for your entertainment.
"..@@.@@@@.\n@@@.@.@.@@\n@@@@@.@.@@\n@.@@@@..@.\n@@.@@@@.@@\n.@@@@@@@.@\n.@.@.@.@@@\n@.@@@.@@@@\n.@@@@@@@@.\n@.@.@@@.@." # You can run against your own input by dragging your file # onto this pane and uncommenting the line below. # &fras"day4.txt" # edit to match the filename. ⊜(=@@)⊸≠@\n N₈ ← ⊂A₂C₂ R ← ×<4⊸(/+⬚0↻)N₈ P₁ ← /+♭R P₂ ← /+♭-⊸⍥(-⊸R)∞ P₃ ← ⍥(▽₃10)<1e6/×⊸△⍥⊸(-⊸R)∞ ⊃(P₁|P₂|P₃)
Now you’re just showing off!

Edit: ooh, this makes it obvious that my puzzle input takes more cycles to reach the done state.
Love a good visualisation <3
I was gonna do the same later when some free time, was wondering if it generated some kind of image.
If you click the link on that post, you’ll see that the test data does resolve to a (very low res) elf!
That’s a great addition :D
Running my own input I also noticed that your solution is a lot faster than mine (processing each roll individually). I’ll keep that 2D-rotation in mind for the future.

Yeah, that’s one thing the gurus keep hammering home: anything you can move out of loop constructs (inc rows, partition, etc as well as the obvious do, repeat) and handle pervasively is a big win.
Haskell
I tried rewriting part 2 to use a MutableArray, but it only made everything slower. So I left it at this. I saw somebody do a 1-second-challenge last year and I feel like that will be very hard unless I up my performance game.
Solution, Both Parts
{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wall #-} module Main (main) where import qualified Data.Text as Text import Data.Array.Unboxed (UArray) import qualified Data.Array.IArray as Array import qualified Data.List as List import Control.Monad ((<$!>), guard) import qualified Data.Text.IO as TextIO import Data.Maybe (fromMaybe) import Control.Arrow ((&&&)) parse :: Text.Text -> UArray (Int, Int) Bool parse t = let gridLines = init $ Text.lines t lineSize = maybe 0 pred $ Text.findIndex (== '\n') t lineCount = Text.count "\n" t - 2 in Array.listArray ((0, 0), (lineCount, lineSize)) $ List.concatMap (fmap (== '@') . Text.unpack) gridLines neighbors8 :: (Int, Int) -> [(Int, Int)] neighbors8 p@(x, y) = do x' <- [pred x .. succ x] y' <- [pred y .. succ y] let p' = (x', y') guard (p /= p') pure p' main :: IO () main = do grid <- parse <$!> TextIO.getContents print $ part1 grid print $ part2 grid part2 :: UArray (Int, Int) Bool -> Int part2 grid = case accessiblePositions grid of [] -> 0 xs -> List.length xs + part2 (grid Array.// fmap (id &&& const False) xs) part1 :: UArray (Int, Int) Bool -> Int part1 = List.length . accessiblePositions accessiblePositions :: UArray (Int, Int) Bool -> [(Int, Int)] accessiblePositions grid = let lookupPosition = fromMaybe False . (grid Array.!?) positions = Array.indices grid paperRollPositions = List.filter lookupPosition positions isPositionAccessible = (< 4) . List.length . List.filter lookupPosition . neighbors8 in List.filter isPositionAccessible paperRollPositionsPython
Simple brute-force is enough.
DIRECTIONS = [ (0, 1), (1, 0), (0, -1), (-1, 0), # cardinal (1, 1), (1, -1), (-1, 1), (-1, -1) # diagonal ] # yield all valid neighbor coordinates def yield_neighbors(i, j, m, n): for di, dj in DIRECTIONS: ni, nj = i+di, j+dj if 0 <= ni < m and 0 <= nj < n: yield ni, nj # build char grid from input data def build_grid(data: str): return [list(row) for row in data.splitlines()] def part1(data: str): grid = build_grid(data) m, n = len(grid), len(grid[0]) # count accessible rolls accessible = 0 for i in range(m): for j in range(n): if grid[i][j] != '@': continue neighbor_rolls = sum(grid[ni][nj] == '@' for ni, nj in yield_neighbors(i, j, m, n)) if neighbor_rolls < 4: accessible += 1 return accessible def part2(data: str): grid = build_grid(data) m, n = len(grid), len(grid[0]) total_accessible = 0 # total accessible rolls over all cycles accessible = None # rolls accessible this cycle # repeat until no more rolls can be accessed while accessible != 0: accessible = 0 for i in range(m): for j in range(n): if grid[i][j] != '@': continue neighbor_rolls = sum(grid[ni][nj] == '@' for ni, nj in yield_neighbors(i, j, m, n)) if neighbor_rolls < 4: accessible += 1 # we can immediately remove this roll, no need to wait for next cycle grid[i][j] = '.' # accumulate accessible rolls this cycle total_accessible += accessible return total_accessible sample = """<paste sample here>""" assert part1(sample) == 13 assert part2(sample) == 43Turns out on part 2 you can remove on access rather than after a full sweep of the grid, which cuts down the number of iterations you need to do about 1/2 sometimes 1/3 (depending on input).
import itertools as it from pathlib import Path import numpy as np cwd = Path(__file__).parent.resolve() def parse_input(file_path): with file_path.open("r") as fp: grid = np.array(list(map(list, list(map(str.strip, fp.readlines())))), dtype=str) return grid def solve_problem(file_name, single_attempt=True): grid = parse_input(Path(cwd, file_name)) nr, nc = grid.shape nacc_total = 0 stop = False while not stop: nacc = 0 for i,j in it.product(range(nr), range(nc)): if grid[i,j] != '@': continue if np.count_nonzero(grid[max(i-1, 0):min(i+2, nr), max(j-1, 0):min(j+2, nc)] == '@')<5: nacc += 1 if not single_attempt: grid[i,j] = '.' nacc_total += nacc if nacc==0 or single_attempt: stop = True return nacc_totalThis is the first day I’ve wished I were working in J, like last year, instead of Lisp. Common Lisp arrays show the age of the language design. They’re basically C arrays with less convenient syntax; if you want higher-order array iteration you have to write it yourself or use a package that provides it. This solution is also less efficient than it could be for part 2, because I recompute the full neighbors array on each iteration rather than updating it incrementally.
(defun read-inputs (filename) (let* ((input-lines (uiop:read-file-lines filename)) (rows (length input-lines)) (cols (length (car input-lines))) (result (make-array (list rows cols) :initial-element 0))) (loop for line in input-lines for y from 0 to (1- rows) do (loop for x from 0 to (1- cols) if (eql (char line x) #\@) do (setf (aref result y x) 1))) result)) (defun neighbors (grid) (let* ((dimensions (array-dimensions grid)) (rows (car dimensions)) (cols (cadr dimensions)) (result (make-array dimensions :initial-element 0))) (flet ((neighbors-at (y x) (loop for dy from -1 to 1 sum (loop for dx from -1 to 1 sum (let ((yy (+ y dy)) (xx (+ x dx))) (if (and (>= yy 0) (< yy rows) (>= xx 0) (< xx cols) (not (and (= dx 0) (= dy 0))) (> (aref grid yy xx) 0)) 1 0)))))) (loop for y from 0 to (1- rows) do (loop for x from 0 to (1- cols) do (setf (aref result y x) (neighbors-at y x)))) result))) (defun main-1 (filename) (let* ((grid (read-inputs filename)) (dimensions (array-dimensions grid)) (neighbors-grid (neighbors grid))) (loop for y from 0 to (1- (car dimensions)) sum (loop for x from 0 to (1- (cadr dimensions)) sum (if (and (> (aref grid y x) 0) (< (aref neighbors-grid y x) 4)) 1 0))))) (defun remove-accessible (grid) (let* ((dimensions (array-dimensions grid)) (neighbors-grid (neighbors grid)) (removed 0)) (loop for y from 0 to (1- (car dimensions)) do (loop for x from 0 to (1- (cadr dimensions)) do (if (and (> (aref grid y x) 0) (< (aref neighbors-grid y x) 4)) (progn (setf (aref grid y x) 0) (incf removed))))) removed)) (defun main-2 (filename) (let ((grid (read-inputs filename)) (removed 0)) (loop for this-removed = (remove-accessible grid) until (zerop this-removed) do (incf removed this-removed)) removed))Javascript
After smashing out a functional version in 20 minutes, I converted it into a OOP approach for a more appealing solution.
Solution
const input = require('fs').readFileSync('input-day4.txt', 'utf-8'); class PrintingDepartmentMap { /** @param {string} input */ constructor(input) { /** @type {number[][]} */ this.map = input.split("\n").map(r => r.split('').map(v => v === '@' ? 1 : 0)); } /** * @param {number} x * @param {number} y * @returns {number} the value of that tile, or -1 if invalid */ getCoord(x, y) { if (x < 0 || y < 0 || x >= this.map[0].length || y >= this.map.length) { return -1; } return this.map[y][x]; } /** * @param {number} x * @param {number} y * @returns {number} the number of adjacent tiles that are >= 1 */ countAdjacent(x, y) { return [ // top-left this.getCoord(x - 1, y - 1) >= 1, // top this.getCoord(x, y - 1) >= 1, // top-right this.getCoord(x + 1, y - 1) >= 1, // right this.getCoord(x + 1, y) >= 1, // bottom-right this.getCoord(x + 1, y + 1) >= 1, // bottom this.getCoord(x, y + 1) >= 1, // bottom-left this.getCoord(x - 1, y + 1) >= 1, // left this.getCoord(x - 1, y) >= 1 ].reduce((acc, v) => !!v ? acc + 1 : acc, 0); } /** * transform in place the map replacing any rolls (1) with (2) if they are accessible * @returns {PrintingDepartmentMap} */ markAccessable() { for (let y = 0; y < this.map.length; y += 1) { for (let x = 0; x < this.map[0].length; x += 1) { if (this.map[y][x] < 1) continue; if (this.countAdjacent(x, y) < 4) { this.map[y][x] = 2; } } } return this; } /** @returns {number} the number of removed rolls */ removeAccessable() { let removed = 0; for (let y = 0; y < this.map.length; y += 1) { for (let x = 0; x < this.map[0].length; x += 1) { if (this.map[y][x] === 2) { removed += 1; this.map[y][x] = 0; } } } return removed; } } const map = new PrintingDepartmentMap(input); let removed = 0; while (true) { const justRemoved = map.markAccessable().removeAccessable(); if (removed === 0) { console.log(`Part 1 Answer: ${justRemoved}`); } if (justRemoved === 0) break; removed += justRemoved; } console.log(`Part 2 Answer: ${removed}`);Haskell
Very simple, this one.
import Data.List import Data.Set qualified as Set readInput s = Set.fromDistinctAscList [ (i, j) :: (Int, Int) | (i, l) <- zip [0 ..] (lines s), (j, c) <- zip [0 ..] l, c == '@' ] accessible ps = Set.filter ((< 4) . adjacent) ps where adjacent (i, j) = length . filter (`Set.member` ps) $ [ (i + di, j + dj) | di <- [-1 .. 1], dj <- [-1 .. 1], (di, dj) /= (0, 0) ] main = do input <- readInput <$> readFile "input04" let removed = (`unfoldr` input) $ \ps -> case accessible ps of d | Set.null d -> Nothing | otherwise -> Just (Set.size d, ps Set.\\ d) print $ head removed print $ sum removedKotlin
Pretty simple solution, just plain count / remove the rolls until none can be removed anymore. I would’ve liked to try using imaginary numbers this year (due to this article), but sadly Kotlin doesn’t natively support them and I was too lazy to use a library.
Solution
class Day04 : Puzzle { val grid = mutableListOf<MutableList<Char>>() override fun readFile() { val input = readInputFromFile("src/main/resources/a2025/day04.txt") for ((r, line) in input.lines().filter(String::isNotBlank).withIndex()) { grid.add(mutableListOf()) for (char in line) { grid[r].add(char) } } } override fun solvePartOne(): String { return countRolls(grid).toString() } override fun solvePartTwo(): String { var sum = 0 var removed = -1 while (removed != 0) { // main grid is modified here, not the best but doesn't really matter // also no idea how to clone 2D list in Kotlin removed = countRolls(grid, true) sum += removed } return sum.toString() } private fun countRolls(grid: MutableList<MutableList<Char>>, removeRolls: Boolean = false): Int { val dr = listOf(-1, -1, -1, 0, 0, 1, 1, 1) val dc = listOf(-1, 0, 1, -1, 1, -1, 0, 1) var sum = 0 for (r in grid.indices) { for (c in grid[r].indices) { if (grid[r][c] != '@') continue var neighborCount = 0 for (i in dr.indices) { if (gridGet(grid, r + dr[i], c + dc[i]) == '@') neighborCount++ } if (neighborCount < 4) { sum++ if (removeRolls) grid[r][c] = '.' } } } return sum } private fun gridGet(grid: List<List<Char>>, r: Int, c: Int, default: Char = '.'): Char { return if (r in grid.indices && c in grid[r].indices) { grid[r][c] } else { default } } }full code on Codeberg
Ha, I’ve got that article half-read in a tab somewhere. Same problem here though - they’re not in the standard library for anything I plan to use for AoC.
Edit: looking at your code, I had forgotten about
.indices. That would have made this a little easier to write.I completely forgot to do the puzzle yesterday somehow. I struggled a bit on this one for a while because I’d used a
<= 4where I should have used a< 4. Just a complete brainfart of thinking, “It needs to be 4 or less”. I wasted more time on that than I’d like to admit.My first stab at this set all of the adjacency counts to 0, and that lead to a few rolls that had no rolls adjacent to them staying on the map by accident.
const val toiletPaperRoll = '@' const val adjacentLimit = 4 var height = 0 var width = 0 fun main() { val input = getInput(4) val map = parseInput1(input) height = map.size width = map[0].size val adjacencyMap = createAdjacencyMap(map) var anyRemoved = true var count = 0 while (anyRemoved) { anyRemoved = false for (y in 0..<height) { for (x in 0..<width) { if (adjacencyMap[y][x] in 0..<adjacentLimit) { count++ anyRemoved = true removeToiletPaperRoll(adjacencyMap, x, y) } } } } println(count) } fun parseInput1(input: String): List<List<Char>> = input .lines() .filter { it.isNotBlank() } .map { it.toCharArray().toList() } fun createAdjacencyMap(map: List<List<Char>>): List<MutableList<Int>> { val adjacencyMap = List(height) { MutableList(width) { -1 } } for (y in 0..<height) { for (x in 0..<width) { if (map[y][x] != toiletPaperRoll) { continue } adjacencyMap[y][x]++ for (y2 in y - 1..y + 1) { for (x2 in x - 1..x + 1) { if (y == y2 && x == x2 || (y2 < 0 || x2 < 0 || y2 >= height || x2 >= width)) { continue } if (map[y2][x2] == toiletPaperRoll) { adjacencyMap[y2][x2]++ } } } } } return adjacencyMap } fun removeToiletPaperRoll(adjacencyMap: List<MutableList<Int>>, x: Int, y: Int) { adjacencyMap[y][x] = -1 for (y2 in y - 1..y + 1) { for (x2 in x - 1..x + 1) { if (y == y2 && x == x2 || (y2 < 0 || x2 < 0 || y2 >= height || x2 >= width)) { continue } if (adjacencyMap[y2][x2] >= adjacentLimit) { adjacencyMap[y2][x2]-- } } } }
Nim
type AOCSolution[T,U] = tuple[part1: T, part2: U] Vec2 = tuple[x,y: int] proc removePaper(rolls: var seq[string]): int = var toRemove: seq[Vec2] for y, line in rolls: for x, c in line: if c != '@': continue var adjacent = 0 for (dx, dy) in [(-1,-1),(0,-1),(1,-1), (-1, 0), (1, 0), (-1, 1),(0, 1),(1, 1)]: let pos: Vec2 = (x+dx, y+dy) if pos.x < 0 or pos.x >= rolls[0].len or pos.y < 0 or pos.y >= rolls.len: continue if rolls[pos.y][pos.x] == '@': inc adjacent if adjacent < 4: inc result toRemove.add (x, y) for (x, y) in toRemove: rolls[y][x] = '.' proc solve(input: string): AOCSolution[int, int] = var rolls = input.splitLines() result.part1 = rolls.removePaper() result.part2 = result.part1 while (let cnt = rolls.removePaper(); result.part2 += cnt; cnt) > 0: discardToday was so easy, that I decided to solve it twice, just for fun. First is a 2D traversal (see above). And then I did a node graph solution in a few minutes (in repo below). Both run in ~27 ms.
It’s a bit concerning, because a simple puzzle can only mean that tomorrow will be a nightmare. Good Luck everyone, we will need it.
Full solution is at Codeberg: solution.nim
C
For loops!
Code
#include <stdio.h> #define GZ 144 static char g[GZ][GZ]; int main() { int p1=0,p2=0, nc=0, x,y; for (y=1; fgets(g[y]+1, GZ-2, stdin); y++) ; for (y=1; y<GZ-1; y++) for (x=1; x<GZ-1; x++) p1 += g[y][x] == '@' && (g[y-1][x-1] == '@') + (g[y-1][x ] == '@') + (g[y-1][x+1] == '@') + (g[y ][x-1] == '@') + (g[y ][x+1] == '@') + (g[y+1][x-1] == '@') + (g[y+1][x ] == '@') + (g[y+1][x+1] == '@') < 4; do { nc = 0; for (y=1; y<GZ-1; y++) for (x=1; x<GZ-1; x++) if (g[y][x] == '@' && (g[y-1][x-1] == '@') + (g[y-1][x ] == '@') + (g[y-1][x+1] == '@') + (g[y ][x-1] == '@') + (g[y ][x+1] == '@') + (g[y+1][x-1] == '@') + (g[y+1][x ] == '@') + (g[y+1][x+1] == '@') < 4) { nc++; p2++; g[y][x] = '.'; } } while (nc); printf("04: %d %d\n", p1, p2); return 0; }For my x86-16 version, the 20K input is pushing it over the 64K .COM limit, so I’ll need to implement some better compression first.
Rust
I pulled out some code from last year to make representing 2D grids as a vector easier, so this was quite straightforward. 2.5ms runtime (including reading/parsing the input twice cos of TDD).
impl Grid { fn neighbour(&self, i: usize, dir: Direction) -> Option<bool> { let width = self.width; let length = self.grid.len(); use Direction::*; match dir { N if i >= width => Some(i - width), NE if i >= width && i % width != width - 1 => Some(i - width + 1), E if i % width != width - 1 => Some(i + 1), SE if i + width + 1 < length && i % width != width - 1 => Some(i + width + 1), S if i + width < length => Some(i + width), SW if i + width - 1 < length && !i.is_multiple_of(width) => Some(i + width - 1), W if !i.is_multiple_of(width) => Some(i - 1), NW if i >= width && !i.is_multiple_of(width) => Some(i - width - 1), _ => None, }; .map(|i| self.grid[i]) } #[rustfmt::skip] fn cell_accessible(&self, i: usize) -> bool { Direction::ALL .iter() .filter(|&&dir| self.neighbour(i, dir).unwrap_or(false)) .count() < 4 } fn num_accessible(&self) -> usize { self.grid .iter() .enumerate() .filter(|&(i, &is_occupied)| is_occupied && self.cell_accessible(i)) .count() } fn remove_accessible(&mut self) -> Option<usize> { let removables = self .grid .iter() .enumerate() .filter_map(|(i, &is_occupied)| (is_occupied && self.cell_accessible(i)).then_some(i)) .collect::<Vec<_>>(); let count = removables.len(); if count > 0 { for idx in removables { self.grid[idx] = false; } Some(count) } else { None } } fn remove_recursive(&mut self) -> usize { let mut total_removed = 0; while let Some(removed) = self.remove_accessible() { total_removed += removed; } total_removed } }Full code
use std::{fs, str::FromStr}; use color_eyre::eyre::{Report, Result}; #[derive(Debug, Copy, Clone)] enum Direction { N, NE, E, SE, S, SW, W, NW, } impl Direction { const ALL: [Direction; 8] = [ Direction::N, Direction::NE, Direction::E, Direction::SE, Direction::S, Direction::SW, Direction::W, Direction::NW, ]; } #[derive(Debug, PartialEq, Eq, Clone)] struct Grid { grid: Vec<bool>, width: usize, height: usize, } impl FromStr for Grid { type Err = Report; fn from_str(s: &str) -> Result<Self, Self::Err> { let grid: Vec<_> = s .chars() .filter_map(|ch| match ch { '.' => Some(false), '@' => Some(true), '\n' => None, _ => panic!("Invalid input"), }) .collect(); let width = s .chars() .position(|ch| ch == '\n') .ok_or_else(|| Report::msg("grid width cannot be zero, or one line"))?; let height = grid.len() / width; Ok(Self { grid, width, height, }) } } impl Grid { fn neighbour(&self, i: usize, dir: Direction) -> Option<bool> { let width = self.width; let length = self.grid.len(); use Direction::*; match dir { N if i >= width => Some(i - width), NE if i >= width && i % width != width - 1 => Some(i - width + 1), E if i % width != width - 1 => Some(i + 1), SE if i + width + 1 < length && i % width != width - 1 => Some(i + width + 1), S if i + width < length => Some(i + width), SW if i + width - 1 < length && !i.is_multiple_of(width) => Some(i + width - 1), W if !i.is_multiple_of(width) => Some(i - 1), NW if i >= width && !i.is_multiple_of(width) => Some(i - width - 1), _ => None, }; .map(|i| self.grid[i]) } #[rustfmt::skip] fn cell_accessible(&self, i: usize) -> bool { Direction::ALL .iter() .filter(|&&dir| self.neighbour(i, dir).unwrap_or(false)) .count() < 4 } fn num_accessible(&self) -> usize { self.grid .iter() .enumerate() .filter(|&(i, &is_occupied)| is_occupied && self.cell_accessible(i)) .count() } fn remove_accessible(&mut self) -> Option<usize> { let removables = self .grid .iter() .enumerate() .filter_map(|(i, &is_occupied)| (is_occupied && self.cell_accessible(i)).then_some(i)) .collect::<Vec<_>>(); let count = removables.len(); if count > 0 { for idx in removables { self.grid[idx] = false; } Some(count) } else { None } } fn remove_recursive(&mut self) -> usize { let mut total_removed = 0; while let Some(removed) = self.remove_accessible() { total_removed += removed; } total_removed } } fn part1(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let grid = Grid::from_str(&input)?; Ok(grid.num_accessible()) } fn part2(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let mut grid = Grid::from_str(&input)?; Ok(grid.remove_recursive()) } fn main() -> Result<()> { color_eyre::install()?; println!("Part 1: {}", part1("d04/input.txt")?); println!("Part 2: {}", part2("d04/input.txt")?); Ok(()) }Rust
fn count_sides(grid: &[Vec<char>], x: usize, y: usize) -> usize { let mut count = 0; for i in y.saturating_sub(1)..=y + 1 { for j in x.saturating_sub(1)..=x + 1 { if i == y && j == x { continue; } if let Some(row) = grid.get(i) { if let Some(col) = row.get(j) { if *col == '@' { count += 1; } } } } } count } #[test] fn test_y2025_day4_part1() { let input = std::fs::read_to_string("input/2025/day_4.txt").unwrap(); let grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut total = 0; let width = grid[0].len(); let height = grid.len(); for y in 0..height { for x in 0..width { if grid[y][x] != '@' { continue; } if count_sides(&grid, x, y) < 4 { total += 1 } } } println!("Total = {total}") } #[test] fn test_y2025_day4_part2() { let input = std::fs::read_to_string("input/2025/day_4.txt").unwrap(); let grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut total = 0; let width = grid[0].len(); let height = grid.len(); loop { let mut iter_total = 0; for y in 0..height { for x in 0..width { if grid[y][x] != '@' { continue; } if count_sides(&grid, x, y) < 4 { grid[y][x] = '*'; iter_total += 1 } } } println!("Moved = {iter_total}"); if iter_total == 0 { break; } total += iter_total; } println!("Total = {total}"); }Nothing really exciting here, was pretty straightforward
Haskell
import Data.Array.Unboxed import Control.Arrow import Data.Foldable type Coord = (Int, Int) type Diagram = UArray Coord Char moves :: Coord -> [Coord] moves pos = (.+. pos) <$> deltas where deltas = [(x, y) | x <- [-1, 0, 1], y <- [-1, 0, 1], not (x == 0 && y == 0)] (ax, ay) .+. (bx, by) = (ax + bx, ay + by) parse :: String -> Diagram parse s = listArray ((1, 1), (n, m)) $ concat l where l = lines s n = length l m = length $ head l isRoll = (== '@') numRolls = length . filter isRoll neighbors d p = (d !) <$> filter (inRange (bounds d)) (moves p) removable d = filter ((<4) . numRolls . neighbors d . fst) . filter (isRoll . snd) $ assocs d part1 :: Diagram -> Int part1 = length . removable part2 d = fmap ((initial -) . fst) . find (uncurry (==)) $ zip stages (tail stages) where initial = numRolls $ elems d stages = numRolls . elems <$> iterate (\x -> x // toX (removable x)) d toX = fmap (second (const 'x')) main = getContents >>= print . (part1 &&& part2) . parseGo
package main import ( "aoc/utils" "fmt" ) type rollMap [][]bool func (rm *rollMap) addRow(line string) { runes := []rune(line) row := make([]bool, len(runes)) for idx, r := range runes { row[idx] = r == '@' } *rm = append(*rm, row) } func (rm rollMap) getAdjacentContents(row, col int) []bool { contents := []bool{} lenrow := len(rm[0]) lenrm := len(rm) for _, i := range []int{-1, 0, 1} { for _, j := range []int{-1, 0, 1} { if i == 0 && j == 0 { continue } r := row + i c := col + j if r >= 0 && r < lenrm && c >= 0 && c < lenrow { contents = append(contents, rm[r][c]) } } } return contents } func countTrue(array []bool) int { count := 0 for _, val := range array { if val { count++ } } return count } func createMap(input chan string) rollMap { rm := rollMap{} for line := range input { rm.addRow(line) } return rm } func stepOne(input chan string) (int, error) { rm := createMap(input) lenrow := len(rm[0]) accessibleRolls := 0 for row := range rm { for col := range lenrow { if rm[row][col] { adj := rm.getAdjacentContents(row, col) numRolls := countTrue(adj) if numRolls < 4 { accessibleRolls++ } } } } return accessibleRolls, nil } type position struct { row, col int } func stepTwo(input chan string) (int, error) { rm := createMap(input) lenrow := len(rm[0]) accessibleRolls := 0 for true { accessedRolls := []position{} for row := range rm { for col := range lenrow { if rm[row][col] { adj := rm.getAdjacentContents(row, col) numRolls := countTrue(adj) if numRolls < 4 { accessibleRolls++ accessedRolls = append(accessedRolls, position{ row: row, col: col, }) } } } } if len(accessedRolls) == 0 { break } else { for _, pos := range accessedRolls { rm[pos.row][pos.col] = false } } } return accessibleRolls, nil } func main() { input := utils.FilePath("day04.txt") ch1, err1 := input.GetLineChannel() if err1 != nil { fmt.Errorf("step one error: %s\n", err1) return } val1, errStep1 := stepOne(ch1) if errStep1 != nil { fmt.Errorf("step one error: %s\n", errStep1) return } fmt.Printf("Step one result: %d\n", val1) ch2, err2 := input.GetLineChannel() if err2 != nil { fmt.Errorf("step two error: %s\n", err2) return } val2, errStep2 := stepTwo(ch2) if errStep2 != nil { fmt.Errorf("step two error: %s\n", errStep2) return } fmt.Printf("Step two result: %d\n", val2) }








