advent of code 2025 solutionsadvent of code 2025 solutions
HappyCoders Glasses

Advent of Code 2025 – Java 25 Solutions

Sven Woltmann
Sven Woltmann
Last update: December 11, 2025

This year I took part in Advent of Code again. In this article you’ll find my solutions. My goal is to implement the solutions as clearly as possible – not necessarily as “clever” or short as possible. I also tried to use as many features of the current Java 25 as possible – including preview features.

If you haven’t heard of Advent of Code: every day there’s a programming puzzle wrapped in a pre-Christmas story. Or rather: two puzzles each day – one that can be solved fairly quickly, and a second one that usually breaks your initial solution due to time or space complexity … forcing you to rethink everything from scratch.

You can find the solutions in this GitHub project: Advent of Code 2025 – Java 25 Solutions.

Here are my solutions for Advent of Code 2015 and Advent of Code 2022.

And now, let’s get started!

Advent of Code 2025 – Day 1 Java Solution

Advent of Code 2025 starts out quite tricky!

We begin with a parser for the rotation:

private static int parseRotation(String s) {
  int multiplicator = s.charAt(0) == 'R' ? 1 : -1;
  return multiplicator * Integer.parseInt(s.substring(1));
}Code language: Java (java)

Part 1

Part 1 is still solved quickly: for each line of the input, we execute the rotations and increment a counter whenever, after the rotation, the pointer is at zero:

position = Math.floorMod(position + rotation, DIAL_SIZE);
if (position == 0) {
  zeroCount++;
}Code language: Java (java)

It is important to use Math.floorMod(…) instead of the % operator, because in Java % does not compute a mathematical modulo, but the remainder of a division. For example, -140 % 100 does not yield 60, but -40. To get 60, we have to call Math.floorMod(-140, 100).

Part 2

For part 2, we have to take a completely different approach. First, we compute the new position without the modulo operation:

position += rotation;Code language: Java (java)

Now we have to distinguish whether we rotated to the right or to the left.
For a right rotation, the calculation is simple: the number of times the pointer passed zero is the position divided by DIAL_SIZE:

if (rotation > 0) {
  zeroCount += position / DIAL_SIZE;
}Code language: Java (java)

For a left rotation, the calculation is more complicated:

else {
  zeroCount += (DIAL_SIZE - position) / DIAL_SIZE;

  boolean startedAtZero = position - rotation == 0;
  if (startedAtZero) {
    zeroCount--;
  }
}Code language: Java (java)

Since we are working with a negative number, we first have to subtract it from DIAL_SIZE. There is also a special case to consider: if the pointer starts at zero, this zero would be counted as well. Therefore, in this case, we need to subtract one.

The else block could also be written more “smartly” like this:

else {
  zeroCount += (DIAL_SIZE - position) / DIAL_SIZE - (position - rotation == 0 ? 1 : 0);
}Code language: Java (java)

However, I consider the first, slightly longer variant to be much more readable.

Finally, we have to “normalize” the position back into the range 0–99:

position = Math.floorMod(position, DIAL_SIZE);Code language: Java (java)

You can find the complete solution here: Advent of Code 2025 – Day 1 Java solution.

Advent of Code 2025 – Day 2 Java Solution

For Day 2, I first write a Range class that can parse one line of the input and return a stream of all numbers in the numeric range (we’ll conveniently be able to reuse this class on later days):

public record Range(long from, long to) {
  public static Range parse(String s) {
    String[] split = s.split("-");
    return new Range(Long.parseLong(split[0]), Long.parseLong(split[1]));
  }

  public LongStream ids() {
    return LongStream.rangeClosed(from, to);
  }
}Code language: Java (java)

Parts 1 and 2 differ in the maximum number of repetitions within an ID. In part 1 there are two repetitions; in part 2 there is no limit – I use Integer.MAX_VALUE.

With that, I can already write a main function:

static long solve(String input, int maxRepetitions) {
  return Stream.of(input.split(","))
      .map(Range::parse)
      .flatMapToLong(Range::ids)
      .filter(id -> isInvalidId(id, maxRepetitions))
      .sum();
}Code language: Java (java)

Now we only need an isInvalidId(…) method.

I’ll show it to you first and explain it afterwards:

private static boolean isInvalidId(long id, int maxRepetitions) {
  String idString = Long.toString(id);
  int idLength = idString.length();
  int minSequenceLength = Math.ceilDiv(idLength, maxRepetitions);
  int maxSequenceLength = idLength / 2;

  for (int sequenceLength = minSequenceLength; 
       sequenceLength <= maxSequenceLength; 
       sequenceLength++) {
    if (containsOnlyRepetitions(idString, idLength, sequenceLength)) {
      return true;
    }
  }

  return false;
}Code language: Java (java)

First, the minimum and maximum length of the repeating subsequence is calculated:

  • The minimum length is the length of the ID divided by the maximum number of repetitions (rounded up). For part 1 this would be half the ID length; for part 2 it is effectively always 1, since maxRepetitions there is Integer.MAX_VALUE.
  • The maximum length is the length of the ID divided by the minimum number of repetitions (rounded down) – and the minimum number of repetitions is 2 in both parts of the task.

Then we iterate from the minimum to the maximum length and check whether the ID consists only of repetitions of subsequences of exactly that length.

That happens in the following containsOnlyRepetitions(…) method:

private static boolean containsOnlyRepetitions(String idString,
                                               int idLength,
                                               int sequenceLength) {
  if (idLength % sequenceLength != 0) {
    return false;
  }

  String sequence = idString.substring(0, sequenceLength);
  for (int i = sequenceLength; i < idLength; i += sequenceLength) {
    if (!sequence.equals(idString.substring(i, i + sequenceLength))) {
      return false;
    }
  }

  return true;
}Code language: Java (java)

At the beginning, we check whether the length of the ID is a multiple of the length of the subsequence. If not, we can immediately return false (this would be the case, for example, if the ID length is 9 and the subsequence length is 2).

After that, we determine the subsequence and use a simple loop to check whether the ID consists only of those subsequences.

You can find the complete solution here: Advent of Code 2025 – Day 2 Java solution.

Advent of Code 2025 – Day 3 Java Solution

Day 3 can be solved with a relatively simple algorithm.

We iterate over the number of batteries we want to connect and, in each iteration, search the battery bank for the first occurrence of the largest remaining number. We start searching to the right of the last battery used and stop at [bank size minus the number of batteries still needed].

I’ll show you this using the example battery bank 234234234234278 (length: 15) and 12 as the number of batteries to be connected:

We search for the largest number within the first four batteries (so that we still have at least eleven batteries available afterward):

234234234234278
^^^^Code language: plaintext (plaintext)

The largest number is 4 at the third position – the batteries before it can be ignored (replaced by dots):

..4234234234278
  ^Code language: plaintext (plaintext)

The remaining eleven batteries must now be found in the remaining range 234234234278 (length: 12). This time, we are only allowed to search within the first two batteries to ensure that at least ten more remain afterward:

..4234234234278 
   ^^Code language: plaintext (plaintext)

The largest number is 3 – again, we can ignore the batteries before it:

..4.34234234278 
    ^ Code language: plaintext (plaintext)

After the 3, ten batteries remain. We need twelve batteries in total, already have two, so we need all ten remaining ones:

..4.34234234278 
     ^^^^^^^^^^Code language: plaintext (plaintext)

This gives us the final result: 434234234278

You can find the implementation of this algorithm here: Advent of Code 2025 - Day 3 Java Solution.

Advent of Code 2025 – Day 4 Java Solution

On Day 4, we can simply simulate the removal of the rolls by the forklift. There’s no need to rack our brains over a special algorithm today.

We iterate over the entire input matrix and count the neighboring rolls for each cell. If there are fewer than 4, we mark the cell as reachable (or store it in a list).

For Part 1, we return the number of reachable cells.

For Part 2, we remove them from the matrix and repeat the entire process for as long as additional rolls can be removed.

You can find my solution here: Advent of Code 2025 - Day 4 Java Solution.

Advent of Code 2025 – Day 5 Java Solution

On Day 5 we can reuse and extend the Range class from Day 1!

For Part 1, we add a contains(…) method, which we can then use to easily check for all ingredients whether they are contained in the ranges of the fresh ingredients:

public record Range(long from, long to) {
  . . .

  boolean contains(long value) {
    return value >= from && value <= to;
  }
}Code language: Java (java)

Part 2 sounds simple at first: Do we just have to add up the lengths of all ranges?

Of course not ;-)

Because the ranges partially overlap – and the ingredients in the overlapping areas would then be counted multiple times.

Instead of subtracting the overlapping areas afterward, I took a different approach: I implemented a RangeSet that is only allowed to contain disjoint ranges – i.e. ranges that do not overlap. All ranges are added to this RangeSet. When adding a new range, the RangeSet checks whether the new range overlaps with any of the existing ones. If it does, the existing range and the new range are merged. Here’s an example:

Ranges im Set:      ....xxxxx........xxxxxx......xxxxx....
Neuer Range:        .......xxxxx..........................

Gemergter Range:        xxxxxxxx

Ranges nach Merge:  ....xxxxxxxx.....xxxxxx......xxxxx....Code language: plaintext (plaintext)

However, it can also happen that the new range overlaps with multiple existing ranges. Therefore, after merging, the newly merged range is checked again for overlaps with the remaining ranges and merged again if necessary. This is repeated until there are no overlaps left:

Ranges im Set:      ....xxxxx........xxxxxx......xxxxx....
Neuer Range:        .......xxxxxxxxxxxxx..................

1. gemergter Range:     xxxxxxxxxxxxxxxx
2. gemergter Range:     xxxxxxxxxxxxxxxxxxx

Ranges nach Merge:  ....xxxxxxxxxxxxxxxxxxx......xxxxx....Code language: plaintext (plaintext)

When merging two ranges, four cases must be distinguished:

  • Range 1 completely contains Range 2
  • Range 2 completely contains Range 1
  • Range 1 overlaps Range 2 from the left (or touches Range 2)
  • Range 2 overlaps Range 1 from the left (or touches Range 1)

After all ranges of fresh ingredients have been added to the RangeSet and all overlaps have been merged, we really do only need to sum up the lengths of all disjoint ranges.

You can find my solution here: Advent of Code 2025 - Day 5 Java Solution.

The Range and RangeSet classes can be found in the common package.

Advent of Code 2025 – Day 6 Java Solution

On Day 6, we have to read numbers from a matrix and then either add or multiply them. In Part 1, we read the numbers row by row, and in Part 2, column by column.

Part 1 is done quickly: we split the line using the regular expression "\s+" (= at least one whitespace character):

private static final Pattern PATTERN = Pattern.compile("\\s+");
. . .
String[] numbers = PATTERN.split(line.strip());Code language: Java (java)

Then we just need to convert the substrings into numbers, collect them in lists for the “problems,” and finally add or multiply all the numbers within these lists.

For Part 2, we do not read the text file from top to bottom, but from left to right (or right to left – both work equally well). Within a column, we read a number by reading the digits from top to bottom. We simply treat spaces as zeros. As soon as we read a zero in an entire column – that is, all rows in that column were empty – we know that we have read all columns of one “problem” and start reading the numbers for the next “problem” from the following column.

The method readNumbersInColumns(…) shows how to iterate over the columns:

private static void readNumbersInColumns(List<String> lines, List<Problem> problems) {
  int columnCount = lines.getFirst().length();
  int problemIndex = 0;

  for (int column = 0; column < columnCount; column++) {
    long number = readNumberInColumn(lines, column);
    if (number != 0) {
      problems.get(problemIndex).addNumber(number);
    } else { // separator column
      problemIndex++;
    }
  }
}Code language: Java (java)

The method readNumberInColumn(…) reads a number from a column by reading the digits from top to bottom:

private static long readNumberInColumn(List<String> lines, int column) {
  long number = 0;

  for (int row = 0; row < lines.size() - 1; row++) {
    char digit = lines.get(row).charAt(column);
    if (digit != ' ') {
      number = number * 10 + (digit - '0');
    }
  }

  return number;
}Code language: Java (java)

You can find the full solution here: Advent of Code 2025 - Day 6 Java Solution.

Advent of Code 2025 – Day 7 Java Solution

Part 1 is solved quickly – basically, we just need to count the shards in the puzzle input.

A trivial solution for Part 2 would be a recursion that checks, for each path a tachyon can take, how many additional possible paths through the facility exist

static long solve(List<String> input) {
  int startPosition = input.getFirst().indexOf('S');
  return solve(input, 2, startPosition);
}

private static long solve(List<String> input, int row, int col) {
  if (row + 2 > input.size()) {
    return 1;
  }
  if (input.get(row).charAt(col) == '^') {
    return solve(input, row + 2, col - 1) + solve(input, row + 2, col + 1);
  } else {
    return solve(input, row + 2, col);
  }
}Code language: Java (java)

However, similar to the recursive computation of the Fibonacci function, this would cause us to recompute the subsequent paths for many positions (which we have reached via different paths) multiple times. The time complexity would be exponential!

A much simpler approach is to store, on the map, the number of timelines by which a point can be reached, and to add up timelines when they converge:

 .  .  .  .  .  .  .  1  .  .  .  .  .  .  .
 .  .  .  .  .  .  1  ^  1  .  .  .  .  .  .
 .  .  .  .  .  .  1  .  1  .  .  .  .  .  .
 .  .  .  .  .  1  ^  2  ^  1  .  .  .  .  .
 .  .  .  .  .  1  .  2  .  1  .  .  .  .  .
 .  .  .  .  1  ^  3  ^  3  ^  1  .  .  .  .
 .  .  .  .  1  .  3  .  3  .  1  .  .  .  .
 .  .  .  1  ^  4  ^  3  3  1  ^  1  .  .  .
 .  .  .  1  .  4  .  3  3  1  .  1  .  .  .
 .  .  1  ^  5  ^  4  3  4  ^  2  ^  1  .  .
 .  .  1  .  5  .  4  3  4  .  2  .  1  .  .
 .  1  ^  1  5  4  ^  7  4  .  2  1  ^  1  .
 .  1  .  1  5  4  .  7  4  .  2  1  .  1  .
 1  ^  2  ^ 10  ^ 11  ^ 11  ^  2  1  1  ^  1
 1  .  2  . 10  . 11  . 11  .  2  1  1  .  1Code language: plaintext (plaintext)

Then, at the end, we only need to add the numbers in the last row to obtain the total number of timelines.

You can find my solution here: Advent of Code 2025 – Day 7 Java solution.

Advent of Code 2025 – Day 8 Java Solution

On Day 8, we have to solve two fundamental challenges:

  • In each iteration, we must find an unused pair of connection points with the shortest distance between them.
  • We must combine pairs of connection points into circuits.

Challenge 1: Finding the remaining pair with the shortest distance

A trivial approach is to store the already used pairs in a set and, in each iteration, calculate the distance between all remaining pairs, selecting the pair with the shortest distance. The time complexity per iteration is O(n²); with m iterations, this results in O(m ⋅ n²).

A slightly better approach is to precompute the distances of all possible pairs and store them in a distance matrix. Then, in each iteration, we can determine the pair with the shortest distance without recalculating distances. This is significantly faster in practice, but the overall time complexity remains unchanged: O(m ⋅ n²).

The best approach is to sort all pairs by their distance upfront. After that, we only need to iterate through the sorted list of pairs. The time complexity is reduced to O(n² + m).

In the code, I created a record that stores a pair of junction boxes (represented by Point3D objects) and the distance between them:

record Point3DPairWithDistance(Point3D first, Point3D second, double distance) {}Code language: Java (java)

The following code converts a list of Point3D objects into a list of these records:

private static List<Point3DPairWithDistance> computePairsWithDistance(
    List<Point3D> points) {
  int numberOfPoints = points.size();
  List<Point3DPairWithDistance> pairs = new ArrayList<>();

  for (int i = 0; i < numberOfPoints; i++) {
    Point3D first = points.get(i);

    for (int j = i + 1; j < numberOfPoints; j++) {
      Point3D second = points.get(j);
      pairs.add(new Point3DPairWithDistance(first, second, first.distanceTo(second)));
    }
  }

  return pairs;
}Code language: Java (java)

This list then only needs to be sorted:

pairs.sort(Comparator.comparing(Point3DPairWithDistance::distance));Code language: Java (java)

Challenge 2: Merging junction boxes into circuits

To merge the junction boxes into circuits, I introduce a CircuitSet class. It stores a list of circuits, where a circuit is a set of connection points, and each connection point is represented by a Point3D object:

private final List<Set<Point3D>> circuits = new ArrayList<>();Code language: Java (java)

All junction boxes are passed to the constructor, which initially creates a separate circuit for each individual junction box:

CircuitSet(List<Point3D> points) {
  for (Point3D point : points) {
    Set<Point3D> circuit = new HashSet<>();
    circuit.add(point);
    circuits.add(newCircuit);
  }
}Code language: Java (java)

To add a pair of junction boxes, I first determine which circuits contain the respective box. If both boxes are already in the same circuit, no further action is required. If they are in different circuits, I merge circuit A into circuit B and then remove circuit A:

void add(Point3DPairWithDistance pair) {
  int firstIndex = -1;
  int secondIndex = -1;

  for (int i = 0; i < circuits.size(); i++) {
    Set<Point3D> circuit = circuits.get(i);
    if (circuit.contains(pair.first())) {
      firstIndex = i;
    }
    if (circuit.contains(pair.second())) {
      secondIndex = i;
    }
  }

  // Connect circuits if both are in different ones
  if (firstIndex != secondIndex) {
    circuits.get(firstIndex).addAll(circuits.get(secondIndex));
    circuits.remove(secondIndex);
  }
}Code language: Java (java)

You can find the complete solution here: Advent of Code 2025 - Day 8 Java Solution.

Advent of Code 2025 – Day 9 Java Solution

On Day 9, Part 1 was once again solved quickly using a brute-force approach: simply calculate the area for all possible combinations of two red tiles and determine their size.

For Part 2, I had to puzzle over it quite a bit longer – and there will certainly be many different solutions here.

First, I created a two-dimensional matrix, “drew” all the edges into it, and then filled the area outside the edges using flood fill. Why outside? That was easier than filling the inside, because I only had to add a border one tile wide around the matrix and then start filling at position (0, 0).

After that, I again used a brute-force approach: for all possible combinations of two red tiles, I checked whether a tile of the rectangle spanned by them lies within the flood-filled area (i.e., outside the red and green tiles). From all rectangles for which this was not the case, I selected the largest one.

This worked well with the example data – but with the real puzzle input, the algorithm hit its limits. The matrix becomes nearly 100,000 × 100,000 in size, i.e. ten billion tiles. Drawing the border was still reasonably fast, but flood fill took several minutes.

I spent a long time thinking about a completely different, more mathematical algorithm – but eventually I came up with the idea of “compressing” the coordinates. After all, out of the 100,000 × 100,000 possible X and Y coordinates, with 496 red tiles at most 496 distinct values can actually be used in each dimension – or even only 248, since two tiles share the same X coordinate, and likewise two tiles share the same Y coordinate.

To do this, I collected all used X and Y coordinates, sorted them, and then created compressed positions from all positions by additionally storing the indices of the X and Y coordinates in their respective sorted coordinate lists.

A compressed position looks like this:

record CompressedPosition(int row, int col, int compressedRow, int compressedCol) {}Code language: Java (java)

And this is the “compression algorithm”:

static List<CompressedPosition> compress(List<Position> positions) {
  Set<Integer> rows = new HashSet<>();
  Set<Integer> cols = new HashSet<>();
  for (Position position : positions) {
    rows.add(position.row());
    cols.add(position.col());
  }

  Map<Integer, Integer> rowIndices = getIndexMap(rows);
  Map<Integer, Integer> colIndices = getIndexMap(cols);

  return positions.stream()
      .map(position -> new CompressedPosition(
          position.row(), position.col(),
          rowIndices.get(position.row()), colIndices.get(position.col())))
      .toList();
}

private static Map<Integer, Integer> getIndexMap(Set<Integer> cols) {
  List<Integer> sorted = new ArrayList<>(cols);
  Collections.sort(sorted);

  Map<Integer, Integer> indexMap = new HashMap<>();
  for (int i = 0; i < sorted.size(); i++) {
    indexMap.put(sorted.get(i), i);
  }

  return indexMap;
}Code language: Java (java)

After that, I was able to run the entire algorithm on the compressed coordinates (size: 248 × 248) and only had to fall back to the uncompressed coordinates when calculating the size of a rectangle.

The brute-force approach of checking all possible combinations of two red tiles, as well as the brute-force check of whether a rectangle contains a tile outside the red–green area, apparently poses no problem at all – the solution for Part 2 now runs in under 100 ms.

You can find my complete solution here: Advent of Code 2025 – Day 9 Java Solution

You can find the coordinate compression in the PositionsCompressor class, the drawing of the border and the flood-fill algorithm in TileFloor, and the calculation of the areas and determination of the largest area in MovieTheater.

Advent of Code 2025 – Day 10 Java Solution

Part 1

On the tenth day, Part 1 is solved quickly once you realize one thing:

Each switch only needs to be pressed at most once – because pressing it a second time reverts the change. Pressing it three times yields the same result as pressing it once, and so on.

The maximum number of button configurations in the puzzle input is 13. That gives us at most 2¹³-1, i.e. 8,191 possible combinations of button presses. Therefore, the algorithm used and the way we store the state of the lights and the button configurations are pretty much irrelevant.

I implemented a breadth-first search, and I stored the state of the lights and the button configurations as a bitset inside an int. This reduces the calculation of a toggle operation to a single XOR operation:

record Lights(int bits) {
  . . .

  Lights toggle(ButtonWiring buttonWiring) {
    return new Lights(bits ^ buttonWiring.bits());
  }
}Code language: Java (java)

In the queue for the breadth-first search I store ComputationState objects, which look like this:

private record ComputationState(Lights lights, int buttonIndex, int depth) {}Code language: Java (java)

A ComputationState object contains the current state of the lights, the index of the next button (in every state we try all buttons to the right of the last button pressed), and the current search depth. The breadth-first search itself is implemented in roughly a dozen lines of code:

int solvePart1() {
  Queue<ComputationState> queue = new ArrayDeque<>();
  queue.add(new ComputationState(new Lights(), 0, 0));

  while (!queue.isEmpty()) {
    ComputationState state = queue.remove();

    for (int i = state.buttonIndex(); i < buttonWirings.size(); i++) {
      Lights lights = state.lights().toggle(buttonWirings.get(i));
      if (lights.equals(lightsGoal)) {
        return state.depth() + 1;
      } else {
        queue.add(new ComputationState(lights, i + 1, state.depth() + 1));
      }
    }
  }

  throw new IllegalStateException("No solution found");
}Code language: Java (java)

You can find my implementation here: Advent of Code 2025 - Day 10 Java Solution.

Part 2

For Part 2 I experimented for several hours and eventually had a heavily optimized algorithm, but after running overnight it had solved only about half of all machines.

Ultimately, the problem is too large to be solved by brute force within a reasonable time – even with the smartest early-exit conditions.

Instead, the problem has to be approached mathematically! If we represent the button configurations as vectors of zeros and ones (bᵢ), and the target state as a vector of joltage levels (j), then we can set up the following equation, where xᵢ are the frequencies with which the respective buttons need to be pressed:

aoc2025 day10 formular

Then we “only” need to find the solution to this equation where the sum x₁ + x₂ + … + xₙ is minimal.

We can solve this equation using Gaussian elimination – however, I haven’t yet had the time to implement it. I may finish this task sometime in December.

The End