Day 3: Lobby
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
import numpy as np def parse_input(file_path): with file_path.open("r") as fp: banks = map(str.strip, fp.readlines()) return map(lambda x: list(map(int, list(x))), banks) def max_jolt(bank, length): if length==1: return max(bank) amax = np.argmax(bank[:-(length-1)]) return 10**(length-1)*bank[amax] + max_jolt(bank[amax+1:], length-1) def solve_problem(file_name, length): banks = parse_input(Path(cwd, file_name)) sumj = 0 for bank in banks: sumj += max_jolt(bank, length) return sumjRust
Seeing some of the other solutions in this thread, there are definitely simpler (and probably still faster) solutions possible, but I first sorted the bank by the highest batteries (keeping the index information) and then used a recursive greedy algorithm to find the largest battery that still follows the index order.
fn part1(input: String) { let mut sum = 0; 'banks: for l in input.lines() { let mut sorted: Vec<(usize, u32)> = l .chars() .map(|c| c.to_digit(10).unwrap()) .enumerate() .collect(); sorted.sort_by(|(_, a), (_, b)| a.cmp(b).reverse()); for (idx, first) in &sorted { for (id2, second) in &sorted { if id2 > idx { sum += first * 10 + second; continue 'banks; } } } } println!("{sum}"); } // Recursive implementation of greedy algorithm. // Returns Vec of length 12 if a result was found, guaranteed to be optimal. // If there is no solution with the input, a shorter Vec is returned. fn recursive(bank: &[(usize, u32)], mut cur: Vec<(usize, u32)>) -> Vec<(usize, u32)> { let pos = cur.last().unwrap().0; for &(idx, e) in bank.iter().filter(|(idx, _)| *idx > pos) { cur.push((idx, e)); if cur.len() == 12 { // Recursion anchor: We have filled all 12 spots and therefore found // the best solution return cur; } // Recurse cur = recursive(bank, cur); if cur.len() == 12 { // Result found return cur; } // Nothing found, try next in this position cur.pop(); } // Unsuccessful search with given inputs cur } fn part2(input: String) { let mut sum = 0; 'banks: for l in input.lines() { let mut sorted: Vec<(usize, u32)> = l .chars() .map(|c| c.to_digit(10).unwrap()) .enumerate() .collect(); sorted.sort_by(|(_, a), (_, b)| a.cmp(b).reverse()); let mut cur: Vec<(usize, u32)> = Vec::with_capacity(12); for &(idx, first) in &sorted { cur.push((idx, first)); cur = recursive(&sorted, cur); if cur.len() == 12 { let num = cur.iter().fold(0u64, |acc, e| acc * 10 + e.1 as u64); sum += num; continue 'banks; } cur.pop(); } } println!("{sum}"); } util::aoc_main!();Go
I usually write a little helper library to bootstrap the input reading process and sometimes even the downloading of the input file. So here it is:
utils.go
package utils import ( "bufio" "os" "strings" ) type Input interface { GetLineChannel() (chan string, error) } type FilePath string type InputText string func (path FilePath) GetLineChannel() (chan string, error) { file, err := os.Open(string(path)) if err != nil { return nil, err } scanner := bufio.NewScanner(file) ch := make(chan string, 1024) go (func() { defer file.Close() for scanner.Scan() { ch <- scanner.Text() } close(ch) })() return ch, nil } func (inputText InputText) GetLineChannel() (chan string, error) { lines := strings.Split(string(inputText), "\n") ch := make(chan string, len(lines)) go (func() { for _, line := range lines { ch <- line } close(ch) })() return ch, nil }And here comes the solution to day 3:
package main import ( "aoc/utils" "errors" "fmt" "math" ) const inputText = `987654321111111 811111111111119 234234234234278 818181911112111` type bank []int func (bk bank) largestNDigitJoltage(n int) int { digits := make([]int, n) count := 0 idx := 0 lenbk := len(bk) for range n { for i := idx; i < lenbk-(n-count-1); i++ { val := bk[i] if val > digits[count] { idx = i + 1 digits[count] = val } } count++ } sum := 0 for index, val := range digits { sum += val * int(math.Pow10(n-index-1)) } return sum } func readBank(line string) (bank, error) { runes := []rune(line) bk := make(bank, len(runes)) for idx, c := range runes { switch c { case '0': bk[idx] = 0 case '1': bk[idx] = 1 case '2': bk[idx] = 2 case '3': bk[idx] = 3 case '4': bk[idx] = 4 case '5': bk[idx] = 5 case '6': bk[idx] = 6 case '7': bk[idx] = 7 case '8': bk[idx] = 8 case '9': bk[idx] = 9 default: msg := fmt.Sprintf("not a number: %c", c) return bank{}, errors.New(msg) } } return bk, nil } func getBankChannel(input chan string) chan bank { ch := make(chan bank, cap(input)) go func() { for line := range input { bank, err := readBank(line) if err != nil { fmt.Errorf("error reading line %v: %v\n", line, err) close(ch) return } ch <- bank } close(ch) }() return ch } func stepOne(input chan string) (int, error) { ch := getBankChannel(input) sum := 0 for bank := range ch { sum += bank.largestNDigitJoltage(2) } return sum, nil } func stepTwo(input chan string) (int, error) { ch := getBankChannel(input) sum := 0 for bank := range ch { sum += bank.largestNDigitJoltage(12) } return sum, nil } func main() { // input2 := utils.InputText(inputText) input := utils.FilePath("day03.txt") ch, err := input.GetLineChannel() if err != nil { fmt.Errorf("step one error: %v\n", err) return } var one int one, err = stepOne(ch) if err != nil { fmt.Errorf("step one error: %v\n", err) return } fmt.Printf("Step one result: %v\n", one) // input2 := utils.InputText(inputText) input2 := utils.FilePath("day03.txt") ch, err = input2.GetLineChannel() if err != nil { fmt.Errorf("step two error: %v\n", err) return } var two int two, err = stepTwo(ch) if err != nil { fmt.Errorf("step two error: %v\n", err) return } fmt.Printf("Step two result: %v\n", two) }While I am quite an adaptable person and I learn to program quickly in about all the languages I’ve tried, I’m still at the beginning of my journey with Go. It does feel like the language is trying to resist me being clever at every corner. I understand the reasons, why not, but damn it does make the development a bit frustrating at times
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)This problem was quite straightforward; I think it probably could have been day 1. The only interesting thing here is the use of
valuesandmultiple-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))))Nim
type AOCSolution[T,U] = tuple[part1: T, part2: U] proc maxJoltage(bank: string, n: int): int = var index = 0 for leftover in countDown(n-1, 0): var best = bank[index] for batteryInd in index+1 .. bank.high-leftover: let batt = bank[batteryInd] if batt > best: (best = batt; index = batteryInd) if best == '9': break # max for single battery result += (best.ord - '0'.ord) * 10^leftover inc index proc solve(input: string): AOCSolution[int, int] = for line in input.splitLines: result.part1 += line.maxJoltage 2 result.part2 += line.maxJoltage 12Runtime: ~240 μs
Day 3 was very straightforward, although I did wrestle a bit with the indexing.
Honestly, I expected part 2 to require dynamic programming, but it turned out I only needed to tweak a few numbers in my part 1 code.Full solution at Codeberg: solution.nim
Haskell
Yay, dynamic programming!
import Data.Map qualified as Map maxJolt :: Int -> [Char] -> Int maxJolt r xs = read $ maximize r 0 where n = length xs maximize = (curry . (Map.!) . Map.fromList . (zip <*> map (uncurry go))) [(k, o) | k <- [1 .. r], o <- [r - k .. n - k]] go k o = maximum $ do (x, o') <- drop o $ zip xs [1 .. n - (k - 1)] return . (x :) $ if k == 1 then [] else maximize (k - 1) o' main = do input <- lines <$> readFile "input03" mapM_ (print . sum . (`map` input) . maxJolt) [2, 12]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]
Haskell
Usually, I get up for AoC, way earlier than I normally would. But today I had to get up at exactly AoC time. I ended up postponing the puzzles until now:
Complete, Dependency-Free and Runnable Program
It reads from stdin and writes the both solutions on a separate line to stdout.
{-# OPTIONS_GHC -Wall #-} import qualified Data.Text.IO as TextIO import Control.Monad ((<$!>)) import qualified Data.Array.Unboxed as Array import qualified Data.Text as Text import qualified Data.Char as Char import Data.Array.Unboxed (UArray) import qualified Data.Foldable as Foldable import Control.Arrow ((&&&)) import qualified Data.List as List parse :: Text.Text -> UArray (Int, Int) Int parse t = let banks = init $ Text.lines t bankSize = maybe 0 pred $ Text.findIndex (== '\n') t bankCount = Text.count "\n" t - 2 in Array.listArray ((0, 0), (bankCount, bankSize)) $ List.concatMap (fmap Char.digitToInt . Text.unpack) banks rowsOf :: UArray (Int, Int) Int -> Int rowsOf = fst . snd . Array.bounds colsOf :: UArray (Int, Int) Int -> Int colsOf = snd . snd . Array.bounds main :: IO () main = do batteryBanks <- parse <$!> TextIO.getContents print $ part1 batteryBanks print $ part2 batteryBanks part1 :: UArray (Int, Int) Int -> Int part1 batteryBanks = Foldable.sum $ pickBatteries 2 batteryBanks <$> [0.. rowsOf batteryBanks] part2 :: UArray (Int, Int) Int -> Int part2 banks = Foldable.sum $ pickBatteries 12 banks <$> [0.. rowsOf banks] pickBatteries :: Int -> UArray (Int, Int) Int -> Int -> Int pickBatteries batteryCount banks row = let width = colsOf banks getBattery col = banks Array.! (row, col) go acc 0 _ = acc go acc n offset = let effectiveEnd = width - pred n availableIndices = [offset .. effectiveEnd] batteryWithIndices = (id &&& getBattery) <$> availableIndices (offset', selectedBattery) = maximumOn snd batteryWithIndices in go (acc * 10 + selectedBattery) (pred n) (succ offset') in go 0 batteryCount 0 maximumOn :: (Foldable t, Ord b) => (a -> b) -> t a -> a maximumOn f collection = case Foldable.toList collection of [] -> error "maximumOn: empty foldable" (x:xs) -> List.foldl selectMax x xs where selectMax a b = if f a < f b then b else aKotlin
First day this year where I am very happy with my solution. Just super simple, recursive string building.
Solution
class Day03 : Puzzle { val banks = mutableListOf<String>() override fun readFile() { val input = readInputFromFile("src/main/resources/a2025/day03.txt") banks.addAll(input.lines().filter(String::isNotBlank)) } override fun solvePartOne(): String { val sum = banks.map { buildNumber(it, 2) }.sumOf { it.toLong() } return sum.toString() } override fun solvePartTwo(): String { val sum = banks.map { buildNumber(it, 12) }.sumOf { it.toLong() } return sum.toString() } private fun buildNumber(bank: String, remainingDigits: Int): String { if (remainingDigits <= 0) return "" val current = bank.dropLast(remainingDigits - 1) val max = current.max() return max + buildNumber(bank.split(max, limit = 2)[1], remainingDigits - 1) } }full code on Codeberg
Today was interesting. My first thought was that part 2 would be a lot more complex at first glance. But I realized that my solution for part 1 worked almost out of the box for part 2.
I was also pleased to see that the algorithm ran in 1ms, which was a good deal faster than just parsing the input.
fun main() { val input = getInput(3) val banks = parseInput(input) var total = 0L banks.forEach { bank -> var location = 0 var joltage = 0L for (power in 11 downTo 0) { val multiplier = 10.toDouble().pow(power).toLong() val batteryLocation = findBattery(bank, location, bank.size - power - 1) val battery = bank[batteryLocation] location = batteryLocation + 1 joltage += battery.toLong() * multiplier } total += joltage } println(total) } fun parseInput(input: String): List<List<Int>> = input .split("\n") .filter { it.isNotBlank() } .map { it.toCharArray() } .map { it.map { digit -> digit.digitToInt() } } fun findBattery(bank: List<Int>, start: Int, end: Int): Int { var max = 0 var location = 0 for (i in start..end) { val battery = bank[i] if (battery > max) { max = battery location = i if (battery == 9) { break } } } return location }Nice! I thought about using numbers but then decided that strings are a lot easier to deal with.
I was curious, so I ran yours and it is only like 3-4ms slower. I was honestly surprised it was that close.
Just goes to show that we’re often wrong when estimating and the only real way to know is to benchmark.
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.
Yeah, I vaguely remember reading something about how close string splitting is to all the logarithm math in splitting numbers, and since then I’ve just always used strings because that’s way more intuitive to me lol
Haskell
I think I could have avoided the minimumBy hack by doing another reverse and changing the indices.
import Data.List import Data.Function import Control.Arrow parse = fmap (fmap (read . pure)) . lines solve n = sum . fmap (sum . zipWith (*) (iterate (*10) 1) . reverse . go n) where go :: Int -> [Int] -> [Int] go 0 l = pure $ maximum l go n l = mx : go (n-1) (drop idx l) where -- use minimumBy since if there are multiple least elements, we want the leftmost one. (idx, mx) = minimumBy (compare `on` (negate . snd)) . zip [1..] . take (length l - n) $ l main = getContents >>= print . (solve 1 &&& solve 11) . parseI’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 }Futhark
I am on my way to re-do all previous days in Futhark and complete the Rest of AoC, hopefully.
def hole: u8 = 0 def zipIndices 'a (xs: []a): [](i64, a) = zip (indices xs) xs def foldMin (xs: []u8): (i64, u8) = let indexedXs = tail (zipIndices xs) in let start = (0, head xs) in foldl (\ (ci, cv) (ni, nv) -> if nv > cv then (ni, nv) else (ci, cv)) start indexedXs def slice 'a (xs: []a) (start: i64) (end: i64) = drop start (take end xs) def pickBattery (bank: []u8) (reserved: i64): (i64, u8) = let batteries = slice bank 0 (length bank - reserved) in foldMin batteries def pickNBatteries (n: i8) (banks: []u8): u64 = let (_, result) = loop (batteries, sum) = (banks, 0) for i in reverse (0...n-1) do let (offset, battery) = pickBattery batteries (i64.i8 i) in (drop (offset + 1) batteries, sum * 10 + u64.u8 battery) in result def part1 (banks: [][]u8): u64 = reduce (+) 0 (map (pickNBatteries 2) banks) def part2 (banks: [][]u8): u64 = reduce (+) 0 (map (pickNBatteries 12) banks) def main (banks: [][]u8) = (part1 banks, part2 banks)Script to Generate input for Futhark
{-# OPTIONS_GHC -Wall #-} {-# LANGUAGE OverloadedStrings #-} import qualified Data.Text.IO as TextIO import Control.Monad ((<$!>)) import qualified Data.Array.Unboxed as Array import qualified Data.Text as Text import qualified Data.Char as Char import Data.Array.Unboxed (UArray) import qualified Data.List as List import qualified Data.ByteString as ByteString import Data.Word (byteSwap64, Word64) import GHC.ByteOrder (ByteOrder(..), targetByteOrder) import qualified Data.Bits as Bits parse :: Text.Text -> UArray (Int, Int) Int parse t = let banks = init $ Text.lines t bankSize = maybe 0 pred $ Text.findIndex (== '\n') t bankCount = Text.count "\n" t - 2 in Array.listArray ((0, 0), (bankCount, bankSize)) $ List.concatMap (fmap Char.digitToInt . Text.unpack) banks rowsOf :: UArray (Int, Int) Int -> Int rowsOf = fst . snd . Array.bounds colsOf :: UArray (Int, Int) Int -> Int colsOf = snd . snd . Array.bounds byteStringLeWord64 :: Word64 -> ByteString.ByteString byteStringLeWord64 word = let leWord = case targetByteOrder of BigEndian -> byteSwap64 word LittleEndian -> word in ByteString.pack . map (fromIntegral . (leWord `Bits.shiftR`)) $ [0,8..56] main :: IO () main = do batteryBanks <- parse <$!> TextIO.getContents putChar 'b' ByteString.putStr (ByteString.singleton 2) -- version ByteString.putStr (ByteString.singleton 2) -- dimensions TextIO.putStr " u8" -- type ByteString.putStr (byteStringLeWord64 . fromIntegral . succ . rowsOf $ batteryBanks) -- outer dim ByteString.putStr (byteStringLeWord64 . fromIntegral . succ . colsOf $ batteryBanks) -- inner dim ByteString.putStr . ByteString.pack . fmap fromIntegral . Array.elems $ batteryBanks -- elementsRust
My first version worked with numbers, but since I was still sick of yesterday’s pow(10) calls, I changed it to use ascii for the second half - the nice thing is that means it can work with hex input with no modification!
Clippy was complaining about “needless_range_loops”, but it’s way more readable this way.
struct PowerSource(Vec<Bank>); impl FromStr for PowerSource { type Err = Report; fn from_str(s: &str) -> Result<Self> { let banks = s.lines().map(|l| Bank(l.to_owned())).collect(); Ok(Self(banks)) } } impl PowerSource { fn max_joltage(&self, num_digits: usize) -> usize { self.0.iter().map(|b| b.max_joltage(num_digits)).sum() } } struct Bank(String); impl Bank { fn max_joltage(&self, num_digits: usize) -> usize { let batts = self.0.as_bytes(); let mut digits = vec![b'0'; num_digits]; let mut start = 0; for d in 0..num_digits { for i in start..=batts.len() - num_digits + d { if batts[i] > digits[d] { digits[d] = batts[i]; start = i + 1; } } } usize::from_str(str::from_utf8(&digits).unwrap()).unwrap() } }deleted by creator








