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

  • Deebster@programming.dev
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    2 days ago

    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(())
    }
    
  • ystael@beehaw.org
    link
    fedilink
    arrow-up
    3
    ·
    1 day ago

    This 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))
    
  • janAkali@lemmy.sdf.org
    link
    fedilink
    arrow-up
    3
    ·
    2 days ago

    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:
        discard
    

    Today 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

  • Zikeji@programming.dev
    link
    fedilink
    English
    arrow-up
    3
    ·
    2 days ago

    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}`);
    
  • Gobbel2000@programming.dev
    link
    fedilink
    arrow-up
    2
    ·
    1 day ago

    Rust

    View on github

    fn parse_input(input: &str) -> Vec<Vec<bool>> {
        input
            .lines()
            .map(|l| l.chars().map(|c| c == '@').collect())
            .collect()
    }
    
    fn count_adj(grid: &[Vec<bool>], (x, y): (usize, usize)) -> usize {
        let width = grid[0].len();
        let height = grid.len();
        grid.iter()
            .take((y + 2).min(height))
            .skip(y.saturating_sub(1))
            .map(|r| {
                r.iter()
                    .take((x + 2).min(width))
                    .skip(x.saturating_sub(1))
                    .take(3)
                    .filter(|e| **e)
                    .count()
            })
            .sum::<usize>()
    }
    
    fn part1(input: String) {
        let grid = parse_input(&input);
        let mut count = 0u32;
        for (y, row) in grid.iter().enumerate() {
            for (x, _) in row.iter().enumerate().filter(|(_, r)| **r) {
                let n_adj = count_adj(&grid, (x, y));
                // Center roll is counted too
                if n_adj < 5 {
                    count += 1;
                }
            }
        }
        println!("{count}");
    }
    
    fn part2(input: String) {
        let mut grid = parse_input(&input);
        let mut removed = 0u32;
        loop {
            let mut next_grid = grid.clone();
            let prev_removed = removed;
            for (y, row) in grid.iter().enumerate() {
                for (x, _) in row.iter().enumerate().filter(|(_, r)| **r) {
                    let n_adj = count_adj(&grid, (x, y));
                    // Center roll is counted too
                    if n_adj < 5 {
                        next_grid[y][x] = false;
                        removed += 1;
                    }
                }
            }
            if removed == prev_removed {
                break;
            }
            grid = next_grid;
        }
        println!("{}", removed);
    }
    
    util::aoc_main!();
    
  • VegOwOtenks@lemmy.world
    link
    fedilink
    arrow-up
    5
    ·
    2 days ago

    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 paperRollPositions
    
  • Avicenna@programming.dev
    link
    fedilink
    arrow-up
    3
    ·
    2 days ago

    Turns 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_total
    
  • Camille@lemmy.ml
    link
    fedilink
    arrow-up
    2
    ·
    1 day ago

    Go

    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)
    }
    
  • CameronDev@programming.devOPM
    link
    fedilink
    arrow-up
    2
    ·
    2 days ago

    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

  • LeixB@lemmy.world
    link
    fedilink
    arrow-up
    2
    ·
    1 day ago

    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) . parse
    
  • Amy@piefed.blahaj.zone
    link
    fedilink
    English
    arrow-up
    3
    ·
    2 days ago

    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 removed  
    
  • eco_game@discuss.tchncs.de
    link
    fedilink
    arrow-up
    2
    ·
    1 day ago

    Kotlin

    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

    • Deebster@programming.dev
      link
      fedilink
      English
      arrow-up
      2
      ·
      1 day ago

      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.

  • Strlcpy@1@lemmy.sdf.org
    link
    fedilink
    arrow-up
    3
    ·
    2 days ago

    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;
    }
    

    Repo

    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.

  • Pyro@programming.dev
    link
    fedilink
    arrow-up
    4
    ·
    edit-2
    2 days ago

    Python

    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) == 43