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
Kotlin
I’m late to the party but I hope some of you will still be inspired by my submisison. This is an iterative solution. I began with a recursive solution that worked but I noticed that it should really be rewritten in an iterative way. The solution is also pointlessly optimized, to some degree, but that’s just what I like to do. 🙂
The logic follows a simple pattern of knowing which window of the battery bank to search in. Given the amount of batteries that remain to be turned on, if you were to turn on the last battery in the window, you’d need to turn on all the remaining batteries. So the window begins at one position past the prior battery and ends at the last battery you actually can choose to turn on. Once that has been turned on, all remaining ones need to be turned on. The window can only actually shrink to at least one position.
Code inside
class Day03 : AOCSolution { override val year = 2025 override val day = 3 override fun part1(inputFile: String): String { return readResourceBinary(inputFile).lineSequence().sumOf { batteryBank -> findHighestJoltage(batteryBank, 2) }.toString() } override fun part2(inputFile: String): String { return readResourceBinary(inputFile).lineSequence().sumOf { batteryBank -> findHighestJoltage(batteryBank, 12) }.toString() } private fun findHighestJoltage( bank: EightBitString, batteries: Int, ): Long { val digitsArray = ByteArray(batteries) { -1 } var lastDigitIndex = 0 repeat(batteries) { currentDigit -> val remainingDigits = batteries - currentDigit val lastIndex = bank.length - remainingDigits + 1 val maxIndex = bank.indexOfMax(lastDigitIndex, lastIndex) lastDigitIndex = maxIndex + 1 digitsArray[batteries - remainingDigits] = bank[maxIndex].toDigit() } return digitsArray.fold(0L) { acc, i -> acc * 10L + i } } private companion object { private fun ByteArray.lineSequence(): Sequence<EightBitString> { val buffer = EightBitString(this) var currentOffset = 0 return generateSequence { for (characterIndex in currentOffset until buffer.limit()) { if (buffer[characterIndex] == '\n') { val slice = buffer.subSequence(currentOffset, characterIndex) // Despite believing that `currentIndex` is not read, // it is indeed read the next time this generator is called. @Suppress("AssignedValueIsNeverRead") currentOffset = characterIndex + 1 return@generateSequence slice } } // A '\n' is always found, because the files end with a new line. return@generateSequence null } } private fun EightBitString.indexOfMax( startIndex: Int, endIndex: Int, ): Int { if (startIndex >= endIndex) { return -1 } var maxIndex = startIndex var max = 0.toByte() for (i in startIndex until endIndex) { val c = getByte(i) if (c > max) { maxIndex = i max = c } } return maxIndex } private fun Char.toDigit(): Byte = (this - '0').toByte() } }Its not a race, its a journey! Keep it up!
(Browser-based) Javascript
For part 2, I eagerly wrote a nice, clean, generic, functional depth-first search, only to get an out of memory error 😭. Note the top-level code blocks: they scope the variables declared inside them, allowing me to run the whole script repeatedly in the console without getting “redeclared variable name” errors.
function part1(inputText) { let totalOutputJoltage = 0; for (const batteryBankDef of inputText.split('\n')) { let bestBankJoltage = 0; const previousDigits = []; for (const character of batteryBankDef) { const currentDigit = Number.parseInt(character, 10); for (const previousDigit of previousDigits) { const possibleVoltage = 10 * previousDigit + currentDigit; if (possibleVoltage > bestBankJoltage) { bestBankJoltage = possibleVoltage; } } previousDigits.push(currentDigit); } totalOutputJoltage += bestBankJoltage; } return totalOutputJoltage; } { const start = performance.now(); const result = part1(document.body.textContent) const end = performance.now(); console.info({day: 3, part: 1, result, time: end - start}) } function findNthDigitForSequence(bankDef, n, startIndex) { let digit = 9; while (digit > 0) { for (let i = startIndex; i < bankDef.length - 11 + n; i++) { if (bankDef[i] === digit.toString()) { return [digit, i] } } digit--; } return undefined; } function findBestJoltageForBank(bankDef) { const digits = []; let previousFoundDigitIndex = -1; for (let i = 0; i < 12; i++) { const digitFound = findNthDigitForSequence(bankDef, i, previousFoundDigitIndex + 1); if (digitFound === undefined) { debugger; return undefined; } const [digit, index] = digitFound; digits.push(digit); previousFoundDigitIndex = index; } return Number.parseInt(digits.join(''), 10); } function part2(inputText) { let totalOutputJoltage = 0; for (const batteryBankDef of inputText.trim().split('\n')) { totalOutputJoltage += findBestJoltageForBank(batteryBankDef) ?? 0; } return totalOutputJoltage; } { const start = performance.now(); const result = part2(document.body.textContent); const end = performance.now(); console.info({ day: 3, part: 2, time: end - start, result }); }Uiua
You can run this in Uiua Pad
"987654321111111\n811111111111119\n234234234234278\n818181911112111" ⊜∘⊸≠@\n Jolt ← ⍥(⊙⊃⋅∘∘+1⟜(⊃(˜⊡|˜↘⊙+₁)⍜↘⟜(˜⨂⊸/↥)))⟜(+1¯) ∩/+⊃≡(⋕Jolt2)≡(⋕Jolt12)Explanation
You’re always looking for the highest digit far enough from the end of the string that there’s space to pick the remaining number of digits. So the algorithm is very straightforward: just temporarily exclude the last n-1 characters off the end of the string and look for the first instance of the largest digit in that substring, store that char and behead the string at that point. Repeat n times.
I cant understand your code, but your description of your algorithm was very clear.
Thanks, although it only really became clear after I’d written it. I am thinking about doing a more detailed explainer post based on one day’s answer to help people get a feel for it.
Rust
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() } }C
Surprise, O(n^12) solutions don’t scale! But then it was delightful when the realization hit that the solution is actually very simple to implement - just keep removing the first digit that is followed by a higher one.
static uint64_t joltage(char *s, int len, int target) { int i; for (; len > target; len--) { for (i=0; i<len-1 && s[i] >= s[i+1]; i++) ; memmove(s+i, s+i+1, len-i); } return strtoul(s, NULL, 10); } int main() { char buf[1024]; uint64_t p1=0,p2=0; int len; while (fgets(buf, sizeof(buf), stdin)) { for (len=0; isdigit(buf[len]); len++) ; buf[len] = '\0'; p2 += joltage(buf, len, 12); p1 += joltage(buf, 12, 2); } printf("03: %lu %lu\n", p1, p2); }I’m still having to finish yesterday’s x86-16 assembly implementation, for which I had to write some support code to deal with large numbers as strings. That will come in useful today, too!
Python
This was the easier one for me out of the first 3 days. Cleaned up my solution before posting for better readability:
# get joltage of picked batteries def get_joltage(batteries_picked: list[int]): bank_joltage = 0 for batt in batteries_picked: bank_joltage = bank_joltage * 10 + batt return bank_joltage # get maximum joltage of a bank def get_bank_joltage(bank: str, pick_limit = 2) -> int: # pick first <pick_limit> batteries batteries_picked = [int(bank[i]) for i in range(pick_limit)] max_joltage = get_joltage(batteries_picked) # iterate over remaining batteries for i in range(pick_limit, len(bank)): batt = int(bank[i]) # we add batt for selection consideration batteries_picked.append(batt) # If all batteries are in descending order and batt is the lowest, # we will eventually discard batt to_discard = pick_limit # However if not, we discard the leftmost MSB battery which has lower joltage than its successor # and shift all batteries left with batt added at the end. # This guarantees that we keep the maximum lexicographical order of picked batteries # regardless of batt's value. for i in range(pick_limit): if batteries_picked[i] < batteries_picked[i+1]: to_discard = i break batteries_picked.pop(to_discard) # update max_joltage, it may have increased max_joltage = max(max_joltage, get_joltage(batteries_picked)) return max_joltage # part 1 asserts assert get_bank_joltage("987654321111111", pick_limit=2) == 98 assert get_bank_joltage("811111111111119", pick_limit=2) == 89 assert get_bank_joltage("234234234234278", pick_limit=2) == 78 assert get_bank_joltage("818181911112111", pick_limit=2) == 92 # part 2 asserts assert get_bank_joltage("987654321111111", pick_limit=12) == 987654321111 assert get_bank_joltage("811111111111119", pick_limit=12) == 811111111119 assert get_bank_joltage("234234234234278", pick_limit=12) == 434234234278 assert get_bank_joltage("818181911112111", pick_limit=12) == 888911112111 # get total joltage of a set of banks def solve(data: str, pick_limit = 2): total_joltage = 0 for bank in data.splitlines(): total_joltage += get_bank_joltage(bank, pick_limit) return total_joltage # asserts for sample data sample = """987654321111111 811111111111119 234234234234278 818181911112111""" assert solve(sample, pick_limit=2) == 357 # part 1 assert solve(sample, pick_limit=12) == 3121910778619 # part 2Haskell
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]
Kotlin
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
c
#include "aoc.h" #include <stdio.h> #include <string.h> constexpr usize LINE_BUFSZ = (1 << 7); constexpr u64 TEN = 10; constexpr u64 TWELVE = 12; static void joltage(strc line, u64* total, usize on) { usize len = strlen(line); usize off = len - on; usize slen = 0; c8 stack[LINE_BUFSZ] = {}; for (usize i = 0; i < len; i++) { while (slen > 0 && off > 0 && stack[slen - 1] < line[i]) { slen--; off--; } stack[slen++] = line[i]; } u64 jltg = 0; for (usize i = 0; i < on; i++) { jltg = (jltg * TEN) + (u64)(stack[i] - '0'); } *total += jltg; } static void solve(u64 on) { FILE* input = fopen("input", "r"); c8 line[LINE_BUFSZ] = {}; u64 total = 0; while (fgets(line, sizeof(line), input)) { line[strcspn(line, "\n")] = 0; joltage(line, &total, on); } fclose(input); printf("%lu\n", total); } i32 main(void) { solve(2); solve(TWELVE); }constexpr u32 TEN = 10; constexpr u64 TWELVE = 12;I love the easter egg jokes you add to your code :)
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
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 sumjNim
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
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 aPython
I had a hunch that part 2 would just be part one with more places and immediately factored that code into a separate method. Overall quite happy with the compact code.
from functools import partial from pathlib import Path from typing import List def parse_input(input: str) -> List[List[int]]: return [list(map(int, l)) for l in input.splitlines()] def find_highest(bank: List[int], n: int) -> int: if n == 1: return max(bank) rest = bank[bank[:-n+1].index(place := max(bank[:-n+1]))+1:] return int(f"{place}{find_highest(rest, n-1)}") def part_one(input: str) -> int: bat_banks = parse_input(input) return sum(map(partial(find_highest, n = 2), bat_banks)) def part_two(input: str) -> int: bat_banks = parse_input(input) return sum(map(partial(find_highest, n = 12), bat_banks)) if __name__ == "__main__": input = Path("_2025/_3/input").read_text("utf-8") print(part_one(input)) print(part_two(input))








