Skip to content

Commit e27804a

Browse files
Add solution blog post for 2025 day 2 (#862)
* Add solution blog post for 2025 day 2 * Fix typos in docs/2025/puzzles/day02.md Co-authored-by: Merlin Hughes <merlin@merlin.org> * simplify regular expressions note * add note about flattening collections * Add a note about optimizing by generating invalid IDs directly * add final code section for 2025 day 2 * Update docs/2025/puzzles/day02.md fix typos in 2025 day 2 post --------- Co-authored-by: Merlin Hughes <merlin@merlin.org>
1 parent ca08b23 commit e27804a

File tree

1 file changed

+99
-2
lines changed

1 file changed

+99
-2
lines changed

docs/2025/puzzles/day02.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,108 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 2: Gift Shop
44

5+
by [@stewSquared](https://github.com/stewSquared)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2025/day/2
810

11+
## Solution Summary
12+
13+
Brute force is sufficient here. We test every number in the ranges for invalid IDs.
14+
15+
### Part 1
16+
17+
First we parse the input string. We collect the range strings by splitting on commas, then we can represent each range with an [Inclusive](https://www.scala-lang.org/api/current/scala/collection/immutable/NumericRange$$Inclusive.html) [`NumericRange`](https://www.scala-lang.org/api/current/scala/collection/immutable/NumericRange.html) from the standard library:
18+
19+
```scala
20+
val ranges = input.split(',').map:
21+
case s"$a-$b" => a.toLong to b.toLong
22+
```
23+
24+
Next, we need to be able to determine if a particular ID is invalid. We can do this by splitting the string representation of the ID into two parts and comparing them:
25+
26+
```scala
27+
def invalid(id: Long): Boolean =
28+
val s = id.toString
29+
val (left, right) = s.splitAt(s.length / 2)
30+
left == right
31+
```
32+
33+
At this point, we can get every ID from the input by flattening our ranges, then we simply filter with `invalid`:
34+
35+
```scala
36+
val ans1 = ranges.iterator.flatten.filter(invalid).sum
37+
```
38+
39+
Note that while `Range` acts like a collection, the individual numbers aren't stored in memory, but when an array of ranges is flattened, it's concretized into an array of numbers, so we first convert with `.iterator` to prevent allocating a full `Array` of all the IDs being checked.
40+
41+
### Part 2
42+
43+
All we need to change is the definition of invalid. Instead of half a string repeated twice, we have a smaller segment of a string repeated multiple times. More specifically, for a proper divisor `d` of the length of the ID `n`, the first `d` characters of the ID are repeated `n/d` times.
44+
45+
Our ID strings are short enough that we can filter possible divisors with modulo, cases where `n % d == 0`. We can then check if a segment repeats by "multiplying" the segment, and comparing it to the original ID. Eg., a string like `"123"`, `"123" * 3 == "123123123"`.
46+
47+
```scala
48+
def invalid2(id: Long) =
49+
val s = id.toString
50+
val n = s.length
51+
val divisors = (1 to n / 2).filter(n % _ == 0)
52+
divisors.exists(d => s.take(d) * (n/d) == s)
53+
```
54+
55+
And now we can use the same line from `ans1` with the updated function:
56+
57+
```scala
58+
val ans2 = ranges.iterator.flatten.filter(invalid2).sum
59+
```
60+
61+
### Alternative: Using Regular Expressions
62+
63+
We can match any sequence of digits using `\d+`. If we place that in a parenthesized group, we can reference it with `\1` to account for repeats. Part 1 looks like so:
64+
65+
```scala
66+
def invalid(id: Long) = """(\d+)\1""".r.matches(id.toString)
67+
```
68+
69+
This first matches any sequence of digits, then succeeds if that sequence is followed by itself. For part 2, we repeat the `\1` match at least once, using `+`:
70+
71+
```scala
72+
def invalid2(id: Long) = """(\d+)\1+""".r.matches(id.toString)
73+
```
74+
75+
### Optimization
76+
77+
While a brute force check of each possible ID works for the provided inputs, an input range could very easily represent gigabytes of Longs. Instead, it's possible to generate invalid IDs directly (eg., start with `123` and multiply by `1001`, `1001001`, etc.). In a solution by [@merlinorg](https://github.com/merlinorg), [such an approach](https://github.com/merlinorg/advent-of-code/blob/789cb88de7e09bc36928b87be685cc95b30e9a4a/src/main/scala/year2025/day02.scala#L30-L42) drops complexity from `O(n)` to `O(sqrt(n))` for part 1.
78+
79+
## Final Code
80+
81+
```scala
82+
import collection.immutable.NumericRange
83+
84+
def part1(input: String): Long =
85+
ranges(input).iterator.flatten.filter(invalid).sum
86+
87+
def part2(input: String): Long =
88+
ranges(input).iterator.flatten.filter(invalid2).sum
89+
90+
def ranges(input: String): NumericRange[Long] =
91+
input.split(',').map:
92+
case s"$a-$b" => a.toLong to b.toLong
93+
94+
def invalid(id: Long): Boolean =
95+
val s = id.toString
96+
val (left, right) = s.splitAt(s.length / 2)
97+
left == right
98+
99+
def invalid2(id: Long): Boolean =
100+
val s = id.toString
101+
val n = s.length
102+
val divisors = (1 to n / 2).filter(n % _ == 0)
103+
divisors.exists(d => s.take(d) * (n/d) == s)
104+
```
105+
9106
## Solutions from the community
10107

11-
Share your solution to the Scala community by editing this page.
12-
You can even write the whole article! [Go here to volunteer](https://github.com/scalacenter/scala-advent-of-code/discussions/842)
108+
- [Solution](https://github.com/stewSquared/advent-of-code/blob/master/src/main/scala/2025/Day02.worksheet.sc) by [Stewart Stewart](https://github.com/stewSquared)
109+
- [Live solve recording](https://www.youtube.com/watch?v=oo1J4u2zATY&list=PLnP_dObOt-rWB2QisPZ67anfI7CZx3Vsq&t=3577s) by [Stewart Stewart](https://youtube.com/@stewSquared)

0 commit comments

Comments
 (0)