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

  •  Pyro   ( @Pyro@programming.dev ) 
    link
    fedilink
    arrow-up
    4
    ·
    edit-2
    1 month 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
    
  •  Zikeji   ( @Zikeji@programming.dev ) 
    link
    fedilink
    English
    arrow-up
    3
    ·
    1 month 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}`);
    
  • 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

  • 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

    •  chunkystyles   ( @chunkystyles@sopuli.xyz ) 
      link
      fedilink
      English
      arrow-up
      2
      ·
      1 month ago

      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 <= 4 where 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]--
                  }
              }
          }
      }
      
  • 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
    
  • 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.

  •  Deebster   ( @Deebster@programming.dev ) 
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    1 month 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   ( @ystael@beehaw.org ) 
    link
    fedilink
    arrow-up
    3
    ·
    1 month 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))
    
  • 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!();
    
  • Uiua

    Quite simple this one. Part 2 still takes a few seconds because I’m essentially checking off each roll individually.

    Run with example input

    Code
    $ ..@@.@@@@.
    $ @@@.@.@.@@
    $ @@@@@.@.@@
    $ @.@@@@..@.
    $ @@.@@@@.@@
    $ .@@@@@@@.@
    $ .@.@.@.@@@
    $ @.@@@.@@@@
    $ .@@@@@@@@.
    $ @.@.@@@.@.
    ⊜∘⊸≠@\n
    =@@
    
    Rolls ← ⍣(⧻⊚˙⍤)∞⊸⊡1_1
    Removable ← (
      ⬚0⧈Rolls[3_3 1_1 1_1]
      ⊚≤4
    )
    
    Remove ← ⍜⊡(˜↯0⧻)
    
    PartOne ← ⧻Removable
    
    PartTwo ← (
      ⊙0
      ⍥(
        ⊙(⊙+⟜⧻)⟜Removable
        ˜Remove
      )∞
      ◌
    )
    &pf "Part One: "
    &p ⊸PartOne
    
    &pf "Part Two: "
    &p PartTwo
    
    Old Part 2

    Before seeing mykl’s solution this was my solution for part 2

    PartTwoOld ← (
      0⊙0
      ⍢(⊙⊙◌
        ⊙⊸Removable
        +⊙⊸⧻
        ⊙⊸Remove
      | ¬≍◌)
      ⊙⋅◌
    )
    

    It’s basically the same, just that I used a while-do-loop, making the check for the ending condition myself (which took me a bit to get right because I still find loops in Uiua a bit confusing).
    Using the repeat-loop as above also gets rid of the dip’s (⊙). I could’ve removed them here as well but I was already deep in the trouble of getting the loop to work correctly and I liked the little face at the beginning 0⊙0

  •  Camille   ( @Camille@lemmy.ml ) 
    link
    fedilink
    arrow-up
    2
    ·
    1 month 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)
    }
    
  •  GiantTree   ( @GiantTree@feddit.org ) 
    link
    fedilink
    arrow-up
    2
    ·
    edit-2
    1 month ago

    Kotlin

    I’m catching up on this year’s AOC.
    This one was rather easy. I already have a pretty versatile grid class that I have just iterated as often as needed.

    Doing this one also lead me into the rabbit hole that is source code generation in Gradle. I used this to generate all the implementations for the primitive types of the grid class as primitive arrays are not generic in the JVM.
    An Array<Int> is an array of integer references, but an IntArray is an array of primitive integers.

    Code on GitHub

    Code
    class Day04 : AOCSolution {
        override val year = 2025
        override val day = 4
    
        override fun part1(inputFile: String): String {
            val grid = readResourceLines(inputFile).map(CharSequence::toList).toCharGrid()
    
            var accessiblePaperRolls = 0
    
            // Quickly iterate the grid in top-left to bottom-right order
            for (y in 0 until grid.height) {
                for (x in 0 until grid.width) {
                    // Count the neighbours of each paper roll.
                    if (grid[x, y] == PAPER_ROLL &&
                        grid.countNeighbours(x, y, 1) { it == PAPER_ROLL } < 4
                    ) {
                        accessiblePaperRolls++
                    }
                }
            }
            return accessiblePaperRolls.toString()
        }
    
        override fun part2(inputFile: String): String {
            val grid = readResourceLines(inputFile).map(CharSequence::toList).toCharGrid()
    
            var count = 0
            while (true) {
                var iterationCount = 0
    
                // Quickly iterate the grid in top-left to bottom-right order
                for (y in 0 until grid.height) {
                    for (x in 0 until grid.width) {
                        if (grid[x, y] == PAPER_ROLL &&
                            grid.countNeighbours(x, y, 1) { it == PAPER_ROLL } < 4
                        ) {
                            // Remove the paper roll for the next iteration
                            grid[x, y] = REMOVED_PAPER_ROLL
                            iterationCount++
                        }
                    }
                }
                count += iterationCount
    
                // Repeat the count until no paper rolls are accessible.
                if (iterationCount == 0) {
                    break
                }
            }
    
            return count.toString()
        }
    
        private companion object {
            const val PAPER_ROLL = '@'
            const val REMOVED_PAPER_ROLL = 'x'
    
            /**
             * Count the neighbours of the given cell in the given [radius] of cells that satisfy the given predicate.
             *
             * @param startX the horizontal position of the center of the count in the grid
             * @param startY the vertical position of the center of the count in the grid
             * @param radius the radius counted in Manhattan distance to the center
             * @param predicate the test that needs to pass for a neighbour to count.
             */
            private fun CharGrid.countNeighbours(
                startX: Int,
                startY: Int,
                radius: Int,
                predicate: Predicate<Char>,
            ): Int {
                var count = 0
                for (y in maxOf(startY - radius, 0)..minOf(startY + radius, height - 1)) {
                    for (x in maxOf(startX - radius, 0)..minOf(startX + radius, width - 1)) {
                        if (y == startY && x == startX) {
                            continue
                        }
                        if (predicate.test(this[x, y])) {
                            count++
                        }
                    }
                }
                return count
            }
        }
    }
    
  •  Amy   ( @lwhjp@piefed.blahaj.zone ) 
    link
    fedilink
    English
    arrow-up
    2
    ·
    1 month 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  
    
  • 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