this post was submitted on 03 Dec 2025
25 points (100.0% liked)

Advent Of Code

1199 readers
1 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

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

all 33 comments
sorted by: hot top controversial new old
[–] mykl@lemmy.world 6 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

Uiua

You can run this in Uiua Pad

"987654321111111\n811111111111119\n234234234234278\n818181911112111"
βŠœβˆ˜βŠΈβ‰ @\n
Jolt ← β₯(βŠ™βŠƒβ‹…βˆ˜βˆ˜+1⟜(βŠƒ(˜⊑|Λœβ†˜βŠ™+₁)βœβ†˜βŸœ(Λœβ¨‚βŠΈ/β†₯)))⟜(+1Β―)
∩/+βŠƒβ‰‘(β‹•Jolt2)≑(β‹•Jolt12)

::: spoiler 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.

[–] CameronDev@programming.dev 3 points 2 weeks ago (1 children)

I cant understand your code, but your description of your algorithm was very clear.

[–] mykl@lemmy.world 2 points 2 weeks ago

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.

[–] Deebster@programming.dev 5 points 2 weeks ago* (last edited 2 weeks ago)

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()
    }
}
[–] tranzystorek_io@beehaw.org 4 points 2 weeks ago* (last edited 2 weeks ago)

Janet

Keep in mind, Codeberg might still be under a DDoS attack

[–] lwhjp@piefed.blahaj.zone 4 points 2 weeks ago (1 children)

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]  
[–] lwhjp@piefed.blahaj.zone 1 points 2 weeks 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]
[–] eco_game@discuss.tchncs.de 4 points 2 weeks ago (1 children)

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

[–] chunkystyles@sopuli.xyz 2 points 2 weeks ago (1 children)

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
}
[–] eco_game@discuss.tchncs.de 2 points 2 weeks ago (1 children)

Nice! I thought about using numbers but then decided that strings are a lot easier to deal with.

[–] chunkystyles@sopuli.xyz 1 points 2 weeks ago (2 children)

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.

[–] eco_game@discuss.tchncs.de 2 points 2 weeks ago

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

[–] Deebster@programming.dev 2 points 2 weeks 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.

[–] Pyro@programming.dev 4 points 2 weeks ago* (last edited 2 weeks ago)

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 2

[–] strlcpy@lemmy.sdf.org 4 points 2 weeks ago* (last edited 2 weeks ago)

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

Repo link

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!

[–] h4x0r@lemmy.dbzer0.com 3 points 2 weeks ago* (last edited 2 weeks ago) (1 children)

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);
}
[–] hades@programming.dev 4 points 2 weeks ago
constexpr u32 TEN = 10;
constexpr u64 TWELVE = 12;

I love the easter egg jokes you add to your code :)

[–] janAkali@lemmy.sdf.org 3 points 2 weeks ago

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 12

Runtime: ~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

[–] Avicenna@programming.dev 3 points 2 weeks ago
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 sumj
[–] VegOwOtenks@lemmy.world 3 points 2 weeks ago

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 ProgramIt 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 a

[–] GiantTree@feddit.org 2 points 1 week ago (1 children)

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()
    }
}

[–] CameronDev@programming.dev 1 points 1 week ago

Its not a race, its a journey! Keep it up!

[–] urandom@lemmy.world 2 points 2 weeks ago* (last edited 2 weeks 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
}
[–] Hammerheart@programming.dev 2 points 2 weeks 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)
[–] VegOwOtenks@lemmy.world 2 points 2 weeks ago

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 -- elements

[–] Quant@programming.dev 2 points 2 weeks ago* (last edited 2 weeks ago)

My original solution for part 1 was just removing the last digit, get the highest number, cut off everything up to and including that first number, get the highest number again.
Once I did part 2 I realized I can just throw in a loop, cut off parts of the end so there's enough numbers left for the subsequent iterations and keep the rest the same.
Now it works for any number of batteries and all you'd need to change is the number after Total! :D

Online pad: AoC-2025-D3

You can even use your own input by uploading a file (make sure it's using LF line endings only with a trailing one at the end) and replacing the example input with this: &rs inf &fo "input-file.txt"

Code

$ 987654321111111
$ 811111111111119
$ 234234234234278
$ 818181911112111
βŠœβˆ˜βŠΈβ‰ @\n

Max ← βŠ’βŠΈβ–

Jolt! ← (
  Β―^
  ""
  β₯(βŠ™(‚⊑Maxβ—‘β†˜+₁
      βŠ™(βŠ™β†˜β€šβ‹…βˆ˜+₁))
    βŠ‚
  )^
  βŠ™β‹…β—Œ
)

Total! ← (
  ≑(β‹•Jolt!^)
  /+
)

PartOne ← Total!2
PartTwo ← Total!12

⊸PartOne
&pf "Part One: "
&p
PartTwo
&pf "Part Two: "
&p

[–] Chais@sh.itjust.works 2 points 2 weeks ago

Python

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))
[–] Jayjader@jlai.lu 2 points 1 week ago

(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 });
}
[–] LeixB@lemmy.world 1 points 2 weeks ago

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) . parse
[–] ystael@beehaw.org 1 points 2 weeks 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))))
[–] Gobbel2000@programming.dev 1 points 2 weeks ago

Rust

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.

View on github

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!();
[–] CameronDev@programming.dev 1 points 2 weeks ago* (last edited 2 weeks ago)
   fn calc_joltage(
        values: &[u32],
        count: usize,
        cache: &mut HashMap<(usize, usize), usize>,
    ) -> usize {
        if let Some(result) = cache.get(&(values.len(), count)) {
            return *result;
        }
        if count == 0 {
            return 0;
        }
        let mut highest = 0;
        let mut highest_base = 0;
        for (i, value) in values[0..values.len() - count + 1].iter().enumerate() {
            if *value < highest_base {
                continue;
            }
            let base_joltage = (*value as usize) * 10_usize.pow(count as u32 - 1);
            let joltage = base_joltage + calc_joltage(&values[i + 1..], count - 1, cache);
            if joltage > highest {
                highest = joltage;
                highest_base = *value;
            }
        }
        cache.insert((values.len(), count), highest);
        highest
    }

    #[test]
    fn test_y2025_day3_part2() {
        let input = std::fs::read_to_string("input/2025/day_3.txt").unwrap();
        let mut total = 0;
        input.lines().for_each(|line| {
            let banks = line
                .chars()
                .map(|c| c.to_digit(10).unwrap())
                .collect::<Vec<u32>>();
            let joltage = calc_joltage(&banks, 12, &mut HashMap::new());
            total += joltage;
        });
        println!("Total: {}", total);
    }

Seems i missed the faster solutions, but i did get this down to a respectable 400ms. edit: 400ms was not respectable, mykl's method took 1ms. Mine was close though, with a bit more brain and optimisation I got there.

And the bot worked all by itself!