Advent of Code - Day 2: Gift Shop


2025-12-02.png

You get inside and take the elevator to its only other stop: the gift shop. “Thank you for visiting the North Pole!” gleefully exclaims a nearby sign. You aren’t sure who is even allowed to visit the North Pole, but you know you can access the lobby through here, and from there you can access the rest of the North Pole base.

-- Day 2 - Advent of Code 2025

Full source can be found: in GitHub

Problem statement

You are given ranges of product IDs like: 998-1012 1188511880-1188511890

An ID is invalid if its digits consist of a smaller sequence repeated:

  • Part 1: exactly twice (e.g. 55, 6464, 123123)
  • Part 2: two or more times (e.g. 111111, 121212, 123123123)

The task is to find all such IDs inside the ranges and compute their sum.

Processing the input

Ranges are parsed once and stored as (lower, upper) pairs.

1private record Range(long lower, long upper) {}
2
3public void readInput() {  
4    for (String range : inputLoader.split(",")) {  
5        String[] bounds = range.trim().split("-");  
6        ranges.add(new Range(Long.parseLong(bounds[0]), Long.parseLong(bounds[1])));  
7    }  
8}

Part 1 solution

A number made of k digits repeated twice looks like:

x followed by x  =>  x * (10^k + 1)

Example:

  • 646464 = 64 × 101
  • 123123123 = 123 × 1001

Instead of iterating over all IDs, we iterate over:

  • digit length k
  • possible base values x
  • and compute valid ranges mathematically

The algorithm

For this algorithm to work the I added the following support methods to the Algo class:

1public static long floorDivPositive(long a, long b) {  
2    // assumes a >= 0 and b > 0  
3    return a / b;  
4}  
5  
6public static long ceilDivPositive(long a, long b) {  
7    // assumes a >= 0 and b > 0  
8    return (a + b - 1L) / b;  
9}

And then used them in the main algorithm:

 1private long sumInvalidIdsInRange(long lower, long upper) {
 2    BigInteger sum = BigInteger.ZERO;
 3
 4    for (int k = 1; k <= 9; k++) {
 5        long pow10k = (long) Math.pow(10, k);
 6        long multiplier = pow10k + 1;
 7
 8        long xMinDigits = pow10k / 10;
 9        long xMaxDigits = pow10k - 1;
10
11        long xFrom = Math.max(
12            xMinDigits,
13            ceilDivPositive(lower, multiplier)
14        );
15        long xTo = Math.min(
16            xMaxDigits,
17            floorDivPositive(upper, multiplier)
18        );
19
20        if (xFrom > xTo) continue;
21
22        BigInteger count = BigInteger.valueOf(xTo - xFrom + 1);
23        BigInteger sumX = BigInteger.valueOf(xFrom)
24            .add(BigInteger.valueOf(xTo))
25            .multiply(count)
26            .divide(BigInteger.TWO);
27
28        sum = sum.add(BigInteger.valueOf(multiplier).multiply(sumX));
29    }
30    return sum.longValue();
31}

Concepts Used

  • Digit length enumeration
  • Closed-form arithmetic series
    $$\sum_{x=a}^{b} x = \frac{(a+b)(b-a+1)}{2}$$
  • Ceiling / floor division to restrict values to the range
  • BigInteger to avoid overflow
    This allows summing millions of candidate IDs in constant time per digit length.

Part 2 solution

Now an invalid ID:

  • Has any repeating block length
  • Repeats at least twice
  • Covers the full number length

Examples:

  • 1111111 repeated 6 times
  • 12121212 repeated 3 times
  • 123123123123 repeated 3 times

Main algorithm

  1. Group numbers by total digit length n
  2. For each n, find all period lengths p where p | n
  3. Compute numbers of the form:
x repeated (n / p) times
  1. Use inclusion–exclusion to ensure each number is counted once, by its minimal period.

Translated into code that would be:

 1private static BigInteger sumInvalidInRange(long lower, long upper) { 
 2    BigInteger total = BigInteger.ZERO;  
 3  
 4    int minLen = digits(lower);  
 5    int maxLen = digits(upper);  
 6  
 7    for (int n = minLen; n <= maxLen; n++) {  
 8        long lo = Math.max(lower, (long) Math.pow(10, n - 1));  
 9        long hi = Math.min(upper, (long) Math.pow(10, n) - 1);  
10        if (lo <= hi) {
11	       total = total.add(sumInvalidFixedLength(n, lo, hi));
12        }  
13    }  
14  
15    return total;  
16}
17
18// Handle the fixed lenght numbers
19private static BigInteger sumInvalidFixedLength(int n, long lo, long hi) {  
20    List<Integer> ps = divisors(n); // sorted  
21    BigInteger[] periodSum = new BigInteger[n + 1];  
22    BigInteger[] minSum = new BigInteger[n + 1];  
23    for (int i = 0; i <= n; i++) {  
24        periodSum[i] = BigInteger.ZERO;  
25        minSum[i] = BigInteger.ZERO;  
26    }  
27  
28    // Compute sums for "has period p" (not minimal yet)  
29    for (int p : ps) {  
30        if (p == n) continue;  
31        periodSum[p] = sumWithPeriod(n, p, lo, hi);  
32    }  
33  
34    // Turn that into "has minimal period p" by subtracting smaller divisor contributions  
35    for (int p : ps) {  
36        if (p == n) continue;  
37  
38        BigInteger s = periodSum[p];  
39        for (int d : ps) {  
40            if (d >= p) break;  
41            if (p % d == 0) {  
42                s = s.subtract(minSum[d]);  
43            }  
44        }  
45        minSum[p] = s;  
46    }  
47  
48    BigInteger total = BigInteger.ZERO;  
49    for (int p : ps) {  
50        if (p == n) continue;  
51        total = total.add(minSum[p]);  
52    }  
53    return total;  
54}
55
56private static BigInteger sumWithPeriod(int n, int p, long lo, long hi) {  
57    int repeats = n / p;  
58    long m = repeatMultiplier(p, repeats);  
59  
60    long xMin = (long) Math.pow(10, p - 1);  
61    long xMax = (long) Math.pow(10, p) - 1;  
62  
63    long from = Math.max(xMin, ceilDiv(lo, m));  
64    long to = Math.min(xMax, floorDiv(hi, m));  
65    if (from > to) return BigInteger.ZERO;  
66  
67    BigInteger count = BigInteger.valueOf(to - from + 1L);  
68    BigInteger a = BigInteger.valueOf(from);  
69    BigInteger b = BigInteger.valueOf(to);  
70  
71    BigInteger sumX = a.add(b).multiply(count).divide(BigInteger.TWO);  
72    return BigInteger.valueOf(m).multiply(sumX);  
73}
$$m=1+10^p+10^{2p}+…$$
 1private static long repeatMultiplier(int p, int repeats) {  
 2    // m = 1 + 10^p + 10^(2p) + ... + 10^((repeats-1)p)  
 3    long tenPowP = (long) Math.pow(10, p);  
 4    long m = 1L;  
 5    long term = 1L;  
 6    for (int i = 1; i < repeats; i++) {  
 7        term *= tenPowP;  
 8        m += term;  
 9    }  
10    return m;  
11}

The final part of the code is to actually loop over all of the ranges and compute the number:

1ranges.stream()  
2        .map(r -> sumInvalidInRange(r.lower, r.upper))  
3        .reduce(BigInteger::add)  
4        .orElseThrow()  
5        .longValue();

See also