[–] 3 points 11 months ago (2 children)

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

  • source
  • [–] 3 points 11 months 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
    
  • source
  • [–] 3 points 11 months ago* (last edited 11 months 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
        }
    }
    

    ::: spoiler 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(())
    }
    
  • source
  • [–] 3 points 11 months 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.

  • source
  • [–] 2 points 11 months ago* (last edited 11 months ago)

    Futhark

    Only part 1 so far, I want to do part 2 later too.

    This is my first ever futhark program. I have not yet figured out whether string parsing is possible or intended with this language. I used a combination of sed and vim to bring the input into a form futhark can read.

    def neighbors (x: i32, y: i32): [8](i32, i32) = [(x+1, y+1), (x+1, y), (x+1, y-1), (x, y+1), (x, y-1), (x-1, y+1), (x-1, y), (x-1, y-1)]
    
    def count 't (p: t -> bool) (xs: []t) : i32 = reduce (+) 0 (map (\ x -> i32.bool (p x)) xs)
    def count2 't (p: t -> bool) (xs: [][]t) : i32 = reduce (+) 0 (map (count p) xs)
    
    def zipIndices [n] 't (xs: [n]t): [n](i32, t) = zip (map i32.i64 (indices xs)) xs
    def zipIndices2 [n][m] 't (xs: [m][n]t): [m][n]((i32, i32), t) = 
      let innerIndices = map zipIndices xs in
      let innerAndOuterIndices = zipIndices innerIndices in
      map (\ (r, a) -> map (\ (c, x) -> ((r, c), x)) a) innerAndOuterIndices
    
    def countIndexed2 't (p: (i32, i32) -> t -> bool) (xs: [][]t): i32 = 
      let withIndices = zipIndices2 xs in
      count2 (\ (i, x) -> p i x) withIndices
    
    type option 't
      = #single t
      | #empty
    
    def safeIndex 't (xs: []t) (i: i32): option t = if i32.i64 (length xs) > i && i >= 0
      then #single xs[i]
      else #empty
    
    def safeIndex2 't (xs: [][]t) ((r, c): (i32, i32)): option t = 
      match safeIndex xs r
        case #single a -> safeIndex a c
        case #empty -> #empty
    
    def orElse 't (o: option t) (d: t): t =
      match o
        case #single x -> x
        case #empty    -> d
    
    def isAccessible (grid: [][]bool) (p: (i32, i32)) (x:bool): bool =
      let neighborsOptions = map (safeIndex2 grid) (neighbors p) in
      let neighborsFilled = map (`orElse` false) neighborsOptions in
      x && count id neighborsFilled < 4
    
    def mapIndexed2 'a 'b (f: (i32, i32) -> a -> b) (xs: [][]a): [][]b =
      let withIndices = zipIndices2 xs in
      map (map (\ (i, x) -> f i x)) withIndices
    
    def removeAccessibles (grid: [][]bool): [][]bool = mapIndexed2 (\ p x -> x && not (isAccessible grid p x)) grid
    
    def part1 (grid: [][]bool): i32 = countIndexed2 (isAccessible grid) grid
    def part2 (grid: [][]bool): i32 =
      let (reducedGrid, _) = 
        loop (current, last) = (removeAccessibles grid, grid)
        while current != last
        do 
          let current' = removeAccessibles current in
          let last'    = copy current in
          (current', last')
      in count2 id grid - count2 id reducedGrid
    
    def main (grid: [][]bool) = (part1 grid, part2 grid)
    
    

    The highlighting is a bit off because I used ocaml as the language. There is no futhark highlighter (at least in Web UI) yet.
    Edit: Part2

    Also, it runs blazingly fast πŸš€ :O

  • source
  • [–] 5 points 11 months ago* (last edited 11 months ago) (5 children)

    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₃)
    

  • source
  • [–] 3 points 11 months 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  
    
  • source
  • [–] 3 points 11 months 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

  • source
  • [–] [S] 2 points 11 months 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

  • source
  • [–] 5 points 11 months 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
    

  • source
  • [–] 3 points 11 months 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}`);
    

  • source
  • [–] 4 points 11 months ago* (last edited 11 months 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
    
  • source
  • [–] 2 points 11 months ago

    I'm really proud of my solution to part 2. When I first read it, I was freaking stumped, I had no idea how to approach it. I was wondering if I was going to wind up resorting to brute forcing it in factorial time. But then I had an idea on the way home, and I one shotted it! (Got a few tests on the example but first try on the actual input)

    from day3_p1 import get_input, get_banks, sample
    
    def turn_on_batteries(bank: string, num_on: int):
        bank_size = len(bank)
        l = 0
        r = bank_size - num_on + 1
        on_bats = []
        while r <= bank_size:
            index_batt_list = list(enumerate(bank))
            index, batt = max(index_batt_list[l:r], key=lambda x: x[1])
            on_bats.append(batt)
            old_l = l
            l = index + 1
            r += 1
        return int("".join(on_bats))
    
    
    actual = get_input("input")
    
    if __name__ == "__main__":
        all_banks = get_banks(actual, "\n")
        res = 0
        for bank in all_banks:
            res += turn_on_batteries(bank, 12)
        print(res)
    

    Part 1 for completeness:

    def get_input(path: str) -> str:
        with open("input") as f:
            data = f.read()
        return data.strip()
    
    def get_banks(data: str, sep=" ") -> list[str]:
        return data.split(sep)
    
    def find_max_battery(bank: str, heap_size=2) -> int:
        batteries = list(enumerate([int(c) for c in bank]))
        first_digit = max(batteries, key=lambda x: x[1])
        if first_digit[0] == len(bank) - 1:
            first_digit = max(batteries[:-1], key=lambda x: x[1])
        second_digit = max(batteries[first_digit[0]+1:], key=lambda x: x[1])
        return first_digit[1] * 10 + second_digit[1]
    
    
    
    sample = "987654321111111 811111111111119 234234234234278 818181911112111" 
    actual = get_input("input")
    
    DATA = actual
    if __name__ == "__main__":
        all_banks = get_banks(DATA, "\n")
        res = 0
        for bank in all_banks:
            res += find_max_battery(bank)
        print(res)
    
  • source
  • [–] 2 points 11 months ago

    My version used strings as well, and I thought that as I was comparing small integers either way, it made sense to stay in ASCII as the strings were already easy to index, and it meant I could skip parsing input numbers, only needing to parse output numbers so they could be summed.

    I did start with numbers so I could convert it back to compare, but it's so fast (the whole thing takes 1ms - and that's reading/parsing the input twice) that it's almost a micro benchmark.

  • source
  • parent
  • context
  • [–] 1 point 11 months ago

    Version 2. I realized last night that my initial approach was way more complicated than it needed to be...

    import Data.List
    import Data.Semigroup
    
    maxJolt :: Int -> [Char] -> Int
    maxJolt r xs = read $ go r (length xs) xs
      where
        go r n xs =
          (\(Arg x xs) -> x : xs) . maximum $
            do
              (n', x : xs') <- zip (reverse [r .. n]) (tails xs)
              return . Arg x $ if r == 1 then [] else go (r - 1) (n' - 1) xs'
    
    main = do
      input <- lines <$> readFile "input03"
      mapM_ (print . sum . (`map` input) . maxJolt) [2, 12]
    
  • source
  • parent
  • context
  • [–] 2 points 11 months ago

    I'm still struggling to learn Rust, and also to figure out a good solution to these problems, but here's mine anyway:

    #[allow(dead_code)]
    pub fn part1(input: Lines<BufReader<File>>) {
        let mut sum = 0;
        for line in input.map_while(Result::ok) {
            let total = find_joltage(line.as_bytes(), 2, false);
            let res = str::from_utf8(total.as_slice()).unwrap();
    
            sum += res.parse::<u32>().unwrap();
        }
    
        println!("{}", sum);
    }
    
    #[allow(dead_code)]
    pub fn part2(input: Lines<BufReader<File>>) {
        let mut sum = 0;
        for (_, line) in input.map_while(Result::ok).enumerate() {
            let total = find_joltage(line.as_bytes(), 12, false);
            let res = str::from_utf8(total.as_slice()).unwrap();
    
            sum += res.parse::<u64>().unwrap();
        }
    
        println!("{}", sum);
    }
    
    fn find_joltage(b: &[u8], size: usize, debug: bool) -> Vec<u8> {
        if size == 0 {
            return vec![];
        }
    
        let mut max: u8 = 0;
        let mut max_i = 0;
        for i in (0..=b.len() - size).rev() {
            if b[i] >= max {
                max = b[i];
                max_i = i;
            }
        }
    
        if debug {
            println!("max = {}, i = {}", max as char, max_i);
        }
        let mut rest = find_joltage(&b[max_i + 1..], size - 1, debug);
    
        rest.insert(0, max);
        rest
    }
    
  • source
  • [–] 1 point 11 months ago

    This problem was quite straightforward; I think it probably could have been day 1. The only interesting thing here is the use of values and multiple-value-bind, which are the standard way in Common Lisp to give a function a primary return value but also one or more advisory return values. This lets you return a rich data structure for consumers who want it, but consumers who don't want it don't need to parse that entire data structure to get the primary return value.

    (defun parse-line (line)
      (let ((digits (loop for i from 0 to (1- (length line))
                          collect (parse-integer line :start i :end (1+ i)))))
        (make-array (list (length digits)) :initial-contents digits)))
    
    (defun read-inputs (filename)
      (let* ((input-lines (uiop:read-file-lines filename)))
        (mapcar #'parse-line input-lines)))
    
    (defun arg-max (v &key start end)
      "Yield the index and the greatest element of vector v, between indices start (inclusive) and
      end (exclusive) if they are supplied. Returns the earliest instance of the maximum."
      (let* ((start (max 0 (or start 0)))
             (end (min (length v) (or end (length v))))
             (arg-max start)
             (val-max (aref v start)))
        (loop for i from start to (1- end)
              if (> (aref v i) val-max)
                do (progn (setf arg-max i)
                          (setf val-max (aref v i)))
              finally (return (values arg-max val-max)))))
    
    (defun bank-max-joltage (digits bank)
      (let ((search-start 0)
            (result 0))
        (loop for d from 0 to (1- digits)
              do (multiple-value-bind (digit-pos digit)
                     (arg-max bank :start search-start :end (+ (length bank) (- digits) (1+ d)))
                   (setf search-start (1+ digit-pos))
                   (setf result (+ (* 10 result) digit))))
        result))
    
    (defun main-1 (filename)
      (reduce #'+ (mapcar #'(lambda (bank) (bank-max-joltage 2 bank))
                          (read-inputs filename))))
    
    (defun main-2 (filename)
      (reduce #'+ (mapcar #'(lambda (bank) (bank-max-joltage 12 bank))
                          (read-inputs filename))))
    
  • source
  • view more: β€Ή prev next β€Ί