We’re working in numbers mod 100. Left turns correspond to subtraction, and right turns correspond to addition. To find the position of the dial after a set of turns we just take the cumulative sum of the additions and subtractions (mod 100). Then we just check how many times this position is zero
Code
data = np.array([50] + [int(x.replace("L", "-").replace("R", "")) for x in load(1)])(np.cumsum(data) %100==0).sum()
Part 2
To find the number of times we’ve crossed zero, we can just look at how many times the hundreds place of the running sum has changed - every time that happens, we’ve crossed the value 0 on the dial. This is almost the right answer but needs to be corrected for the fact that sometimes we just kiss zero and don’t cross it. For example, if the dial went 10 -> 0 -> 10, the divisor test wouldn’t show a zero crossing. Similarly, if we went 90 -> 0 -> 90, the divisor test would show two crossings instead of one.
A bit of thinking shows that the relevant points are those where
The total sum is zero after a move
The dial moves in the opposite way in the next move.
We can find both of those cases using numpy magic, and the necessary expression then becomes
There’s probably a clever approach here, but I’m not seeing it right now – I’m guessing that comes with adventing at 9pm after a long day at work. I’ll just iterate over the ranges, and for each range, iterate over the numbers in the range. For every number in the range, I’ll check whether str(number) == str(number)[:length//2] * 2. It’s slow, but it works
Code
ranges = [[int(y) for y in x.split("-")] for x in load(2)[0].split(",")]def invalid_ids(part=1): total =0for val in itertools.chain(*[range(start, end +1) for start, end in ranges]): length =len(str(val)) repeats = [2] if part ==1else proper_factors(length)ifany(str(val)[: length // repeat] * repeat ==str(val) for repeat in repeats): total += valreturn totalinvalid_ids()
Part 2
The advantage of organising part 1 like that is that part 2 can be included in the same function with a small flag to switch between the two code paths. The only thing that’s left to do is to implement a factorisation function and we are done.
Code
@functools.cachedef proper_factors(n): f = [list(set((i, n // i))) for i inrange(2, int(math.sqrt(n)) +1) if n % i ==0]returnsorted([x for pair in f for x in pair] + ([n] if n !=1else []))invalid_ids(part=2)
Source Code
---title: 2025 Solutions---# Imports```{python}# | eval: true# | output: falseimport dataclassesimport functoolsimport itertoolsimport mathimport operatorimport osimport reimport sysfrom collections import defaultdict, deque, namedtuplefrom queue import PriorityQueueimport more_itertoolsimport numpy as npimport pandas as pdimport scipysys.path.insert(1, "..")import utilsload = utils.year_load(2025)```# [Day 1: Secret Entrance](https://adventofcode.com/2025/day/1)## Part 1We're working in numbers mod 100. Left turns correspond to subtraction, and right turns correspond to addition. To find the position of the dial after a set of turns we just take the cumulative sum of the additions and subtractions (mod 100). Then we just check how many times this position is zero```{python}data = np.array([50] + [int(x.replace("L", "-").replace("R", "")) for x in load(1)])(np.cumsum(data) %100==0).sum()```## Part 2To find the number of times we've crossed zero, we can just look at how many times the hundreds place of the running sum has changed - every time that happens, we've crossed the value 0 on the dial. This is almost the right answer but needs to be corrected for the fact that sometimes we just kiss zero and don't cross it. For example, if the dial went `10 -> 0 -> 10`, the divisor test wouldn't show a zero crossing. Similarly, if we went `90 -> 0 -> 90`, the divisor test would show two crossings instead of one.A bit of thinking shows that the relevant points are those where1. The total sum is zero after a move2. The dial moves in the opposite way in the next move.We can find both of those cases using numpy magic, and the necessary expression then becomes```{python}sum(abs(np.diff(np.cumsum(data) //100))+ ((np.cumsum(data) %100==0)[:-1] * np.diff(np.sign(data)) //2))```# [Day 2: Gift Shop](https://adventofcode.com/2025/day/2)## Part 1 {#part-1-1 id="243d6f36-f834-4365-988d-96eb25722e56"}There's probably a clever approach here, but I'm not seeing it right now – I'm guessing that comes with adventing at 9pm after a long day at work. I'll just iterate over the ranges, and for each range, iterate over the numbers in the range. For every number in the range, I'll check whether `str(number) == str(number)[:length//2] * 2`. It's slow, but it works```{python}ranges = [[int(y) for y in x.split("-")] for x in load(2)[0].split(",")]def invalid_ids(part=1): total =0for val in itertools.chain(*[range(start, end +1) for start, end in ranges]): length =len(str(val)) repeats = [2] if part ==1else proper_factors(length)ifany(str(val)[: length // repeat] * repeat ==str(val) for repeat in repeats): total += valreturn totalinvalid_ids()```## Part 2The advantage of organising part 1 like that is that part 2 can be included in the same function with a small flag to switch between the two code paths. The only thing that's left to do is to implement a factorisation function and we are done.```{python}@functools.cachedef proper_factors(n): f = [list(set((i, n // i))) for i inrange(2, int(math.sqrt(n)) +1) if n % i ==0]returnsorted([x for pair in f for x in pair] + ([n] if n !=1else []))invalid_ids(part=2)```