Advent of Code 2025; Day 3: Lobby

Day 3 brings battery banks and joltage calculations to restore emergency power to the escalator.

Part 1: Maximum Joltage

The elf has informed me that the main elevators as well as the escalator are out of service due to a power surge. Great.

Luckily, they seem to have an emergency battery supply to get the escalator back up and running. Yay for the business continuity plan!

The emergency batteries are arranged in banks. I need to turn on two batteries that achieve the highest joltage. Bank joltage is determined by concatenating the two highest battery joltages - without rearranging the batteries in the bank.

1
2
3
4
987654321111111
811111111111119
234234234234278
818181911112111

The example would be 98 for the first bank, as the first highest joltage is 9 followed by 8. The second bank would be 89; although the highest value is 9 we must turn on two batteries in the bank, and they must be turned on in order so the 9 must be turned on after 8. The next two are 78 and 92. The total joltage is then the sum of all the bank joltages; 357.

Reading in the input this time isn’t particularly interesting; I’ve not felt the need to do any extra processing on the data like previous days. Just a straight up line split to separate all the battery banks is all I need.

Although I did just realise there is a splitlines function which I hadn’t been using up until now.

python
1
2
def load_battery_banks(input_data: str) -> list[str]:
    return input_data.strip().splitlines()

I’ll admit that today I went with a quick and dirty approach which I then cleaned up a little - the scan function I use here was just inline and copy pasted but it hurts me to look at it and I won’t subject anyone to code like that.

The function to determine joltage of an individual bank just needs to scan for the highest value, crucially remembering that I need two batteries per bank, so cannot use the last battery in the bank as the first value.

This is easy enough to do in python with slices. There is a neat feature where you can use negative indexes to index relative to the last index. So I can do bank[:-1] to get all but the last battery cell for the first scan.

The key bit then is my scan needs to return not just the highest joltage in the set, but also the index of that value. I need this to determine the start of the next slice to the end of the bank to scan for the second battery. Now I could possibly have avoided that using recursion - but then I think I’d have issues keeping track of the full string at each level deeper, because it needs to expand the range at the end. I’ll just forget that and continue as the non-recursive option seems simpler to me in this case.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def joltage(bank: str) -> int:
    j, i = scan(bank[:-1])
    j2, _ = scan(bank[i + 1:])
    return int(j + j2)

def scan(bank: str) -> tuple[str, int]:
    index, joltage = 0, 0
    for i, cell in enumerate(bank):
        cell = int(cell)

        # does this cell have a higher joltage?
        if cell > joltage:
            joltage = cell
            index = i

    # return cell joltage and its bank index
    return (str(joltage), index)

Putting this together I can revisit some of the more pythonic ways I have learned so far and use a generator into a sum:

python
1
2
3
4
5
def part1(input_data: str) -> int:
    battery_banks = load_battery_banks(input_data)
    return sum(
        joltage(i) for i in battery_banks
    )

Emergency power restored!

Part 2: Emergency Joltage Safety Override

OK, so it turns out the joltage from the emergency power supply isn’t enough to overcome the static friction of the escalator; the elf has enabled the safety override.

I need to have another go, but this time without the safety override enabled I need to turn on a total of 12 batteries per bank! Again determining which battery cells will result in the maximum joltage possible.

So first things first, I think this is essentially the same algorithmically speaking that I have done for the first two battery cells but with the ability to tweak more variables, i.e. the number of times it scans. But what that also then requires is a variable exclusion at the tail end of the bank (where previously I just did [:-1]), which would need to decrease after each scan (because we need to reserve 1 less battery each time we turn one on) effectively sliding the window towards the end.

If I change the joltage function to take the number of cells to turn on as a parameter, I should be able to recreate the initial Part 1 result with a cell count of 2.

python
1
2
3
4
5
6
7
8
def joltage(bank: str, cells: int) -> int:
    joltage = ""
    index = 0
    for x in range(cells-1, -1, -1):
        j, i = scan(bank[index:len(bank)-x])
        index += i + 1
        joltage += j
    return int(joltage)

If I talk about replicating the two cell joltage first, the range for x in range(cells-1, -1, -1) is effectively counting down from 1 to 0 i.e. two iterations; range will stop at the value -1 and not iterate the loop, so the last value of x would be 0. I do this in reverse because after each loop we have turned on a battery and therefore can reduce how many we reserve at the end by -1.

The value of x is then used to slice off the end of the bank that we are going to scan, when x==1 it’s the equivalent of [:-1].

But this is where I got caught out.

When x==0, slicing as I did in Part 1 with [:-x] doesn’t work as it’s effectively doing [:0] - I get an empty bank to scan and therefore 0 appended to the joltage. Using len(bank) explicitly prevents this as len(bank)-0 will slice to the end and not 0.

There’s another change to consider, the index value used as the start of the slice on the bank for each scan. When scan receives a slice of the bank, it only knows the index of the battery within that slice. This is where index += i + 1 comes in - because each slice is sequential starting from the last index I can just add i returned from the scan and offset it to match the index in the full bank (+1 is just to start the next slice exclusive of the battery we just indexed).

Append the joltages together each time and it’s now replicated the two scans I hard coded originally. It’s as simple as changing the cells parameter to 12 and the emergency override kicks in.

python
1
2
3
4
5
def part2(input_data: str) -> int:
    battery_banks = load_battery_banks(input_data)
    return sum(
        joltage(i, 12) for i in battery_banks
    )

More Pythonic? Or Just Better Code

As seems to be the standard process when I write python, I’m left wondering if I could have done it differently.

My first thoughts are the scan function I created - this is likely a common pattern and python tends to cater to common patterns. Turns out it can be simplified quite a lot:

python
1
2
3
def scan(bank: str) -> tuple[str, int]:
    index, joltage = max(enumerate(bank), key=lambda x: int(x[1]))
    return joltage, index 

This removes all of my casting, tracking and looping with a single function call, of course it does. Essentially, max does what I did to find the largest value in the slice, and I’m not at all surprised it exists. I just avoided it because I knew I also needed to get the index, but combining that with enumerate and keying to the battery value (x[0]==battery_index, x[1]==battery_joltage) and you get the max value, crucially with the enumerated index. Excellent!

To be fair, I could do the same thing in other languages I use; Linq in C#, and iterators in Rust also have that capability.

The next bit that screams at me is the joltage concatenation in Part 2. I’m thinking there must be a way to use a generator or something there? I’m not sure there’s much benefit to changing it as it is actually quite readable. Maybe a list / join instead of appending the string each time.

Actually that’s not really exclusive to python, it’s just better code.

As always, full code is available at github.com/lordmoocow/aoc25.


Not too bad today, although it took me a moment to understand how the requirements of part 2 still enforced the order of which batteries were turned on. Once again I have had my obligatory bug rabbit hole on something seemingly trivial. 9 days to go.