<Advent of Code 2025 day 2> (spoilers) :thread:
# advent-of-code
a
p
Regex for part 2 felt like the easy choice if I hadn't stumbled by inverting my filter.
❤️ 1
j
just toLongs and toStrings. and asSequences and chunkeds. and anys and alls 🙂
Copy code
private fun solve(data: String, filterOp: (String) -> Boolean): Long =
    data.trim().split(",").map { it.split("-").let { (a, b) -> a.toLong()..b.toLong() } }
        .flatMap { it.asSequence() }
        .map { it.toString() }
        .filter(filterOp)
        .sumOf { it.toLong() }

fun part1(data: String): Any = solve(data) { s ->
    s.length % 2 == 0 && s.take(s.length / 2) == s.takeLast(s.length / 2)
}

fun part2(data: String): Any = solve(data) { s ->
    (1..s.length / 2).any { c ->
        s.chunked(c).let { l -> l.all { p -> p == l.first() } }
    }
}
n
Yeah, this was much simpler than Day 1. BTW: I do not think you need the
^
and
$
in your regex because
matches
anyway only returns
true
if it matches the whole input.
m
Part 1
Part 2
I don't like that the example was formatted differently (multiple lines) from the real input (single line). It did clearly say so, but I had to take that in mind.
n
Copy code
private fun parse(input: List<String>) =
        input[0].split(',').map { it.split('-').map { it.toLong() }.let { it[0]..it[1] } }

    fun one(input: List<String>): Long {
        val rx = Regex("""(\d+)\1""")
        return parse(input).flatMap { r -> r.filter { rx.matches(it.toString()) } }.sum()
    }

    fun two(input: List<String>): Long {
        val rx = Regex("""(\d+)\1+""")
        return parse(input).flatMap { r -> r.filter { rx.matches(it.toString()) } }.sum()
    }
👍 1
y
Idk why but I love starting my solution from creating supporting structures that. For some reason it make it easier for me to think about the task. Day1-Day2 doesn't matter that much, but in the last days I am not able to solve tasks without them
Btw @Marcin Wisniowski I took a look at your black magic with filename / day number from stacktrace and now I'm trying to hold myself from implementing the same thing for Kotlin Scripting (I solve this aoc with it). Because in Kotlin Scripting this is much easier since they expose
__FILE__
property that is
java.io.File
a
Not too hard again but... just realized Kotlin Notebook doesn't have debugging ability 🤥
🔥 1
y
@Andriy but isn't notebook itself a debugging ability? you may split by cells with the precision you want
m
regex to check for repeats was a great find guys, smart!
🙌 1
j
BTW here's my approach to event / day number for fetching data - thanks to it, every puzzle's file looks the same (sans the package name) and I'm quite happy with it 🙂 (except the
= catching {
part, I did it only so the exception does not cause run to fail and scroll the intellij output to meaningless gradle error)
a
@y9san9 not sure... will try... thank you!
a
need some help. part2 works with the test input but not the actual input. wondering if my input has some edge case. 🤦 I already submitted the answer and thought it was "wrong". so tried small changes and kept re-submitting and getting same message - not noticing it said "You don't seem to be solving the right level. Did you already complete it?" also using regex:
Copy code
private fun isInvalidPartTwo(num: Long) = """^(\d+)\1+$""".toRegex().matches(num.toString())
used with `data: List<ListLong>`:
Copy code
fun partTwo(): Long =
    data.flatMap {
        LongProgression.fromClosedRange(it.first(), it.last(), 1L)
            .filter(::isInvalidPartTwo)
    }.sum()
b
i didn't think of regex, simple mutable solution for me, which seems to be faster than the chunked variant:
Copy code
ranges.sumOf { r ->
                r.sumOf {
                    val s = it.toString()
                    if ((1..s.length / 2).any { i ->
                            val p = s.take(i)
                            var left = s.drop(i)
                            while (left.isNotEmpty() && (left.take(i) == p)) {
                                left = left.drop(i)
                            }
                            left.isEmpty()
                        }) it else 0
                }
            }
y
drop and take are still going to create new parts as chunked would. there is no difference in the best case and it might be implemented worse than chunked
b
not sure, on my machine replacing it with chunked was 2-2.5x slower:
Copy code
recursive took 139.981438ms (1.00x): 46666175279
	 first took 148.612785ms (1.06x): 46666175279
	 chunk took 412.768798ms (2.95x): 46666175279
d
I did an optimized search from the beginning — I only check duplicated numbers. Fun problem. https://github.com/dfings/advent-of-code/blob/main/src/2025/problem_2.main.kts
y
@Jakub Gwóźdź very cool approach. Now I want to build an interactive CLI app that schedules the start of the day, shows timer then fetches page, coverts it to markdown, puts input file in place and you are ready to solve everything. But I don't have time for this 😞
👍 1
n
@Anirudh: try using
flatMap { ... }.distinct().sum()
, perhaps your input produces the same invalid number more than once.
d
I assumed the ranges would be too big and didn’t even implement check each number in the range 😂
a
@Norbert Kiesel thanks. but it worked already. I posted that message 1hr 3mins after AoC started. but according to my personal times, I already submitted the right answer at 0h 54mins 🙃 🤦 on the submission page, I thought I was getting "wrong answer" message. so tried small changes and kept re-submitting and getting same message - not noticing it said "You don't seem to be solving the right level. *Did you already complete it?*"
n
Good. But I still think that the
.distinct()
is relevant. My understanding is that a
95-115,99-112
should only return
99
and not
198
because we should return the sum of all invalid numbers, and the only invalid number in both of these ranges is
99
.
a
that's a fair point but since he makes no mention of overlaps or repeats in the challenge, it's open to interpretation. until someone has an input with "overlapping ranges and repeating invalid IDs" and gets the right answer with/without distinct. I didn't check my input for overlaps and not using
distinct
.
e
cannot get it any more compact than this without making it unreadable:
Copy code
fun main() {
    solve("Gift Shop") {
        val input = lines[0]
            .split(",")
            .map { it.split("-") }
            .map { (f, s) -> f.toLong()..s.toLong() }

        fun calc(check: Long.() -> Boolean) = input.sumOf { r -> r.filter { check(it) }.sum() }

        part1(-1) {
            calc { "$this".let { s -> (s.length / 2).let { s.take(it) == s.drop(it) } } }
        }

        part2(-1) {
            calc { "$this".matches(Regex("(.+)\\1+")) }
        }
    }
}
n
not using
.distinct()
also did not make a difference for my input, but "find all of the invalid IDs" is the real challenge. It's just that we then return the sum of them as the single answer.
d
The way it’s described sort of implies that the ranges are non-overlapping (it’s the list of ranges that haven’t been checked yet), which seems to be the case for real input. However I needed to use distinct/toSet on part 2 within each range because of how I generated the matching numbers.
a
@Norbert Kiesel @Dan Fingal-Surma there's usually some ambiguity in the challenge. for example, in last year's short-circuit race thing (checked, Day 20) > Each cheat has a distinct start position (the position where the cheat is activated, just before the first move that is allowed to go through walls) and end position; I thought this implies that the first cheat move has to 'be at the wall' (start position is on the path). this matters more in part 2 where I kept getting the wrong answer (test input worked I think). I was checking my first cheat move being at a wall. once I removed that check, I got the right answer without other changes. I don't know if everyone else who got it right consciously decided not to check because it's not needed or just didn't interpret it that way or just didn't notice that "requirement". (quotes because it wasn't a requirement)
d
Did a minor optimization to discard repetition counts that can’t possibly generate matches. Now, looking at my input, the ranges contain
2579792
numbers and I test
908
for inclusion in part 1 and
1039
in part 2, of which
865
and
979
match, respectively.
O(sqrt(n))
which should be optimal time complexity.
Screenshot 2025-12-01 at 11.41.13 PM.png
k
The solution utilizes a lazy evaluation strategy (Kotlin
Sequence
) to handle large input streams with O(1) memory usage, preventing memory overflows on large single-line files. Part 1: The "Double Pattern" (Exact Halves)Logic: A number is valid if it can be split into exactly two identical halves (e.g.,
1212
). • Algorithm: a. Check if the digit count L is even. b. Split the string at index L / 2. c. Compare the first half substring with the second half substring. • Time Complexity: O(L) per number, where L is the number of digits. String slicing and comparison iterate over the digits once, making it linear relative to the digit count. Part 2: The "Multi Pattern" (Repetition N >= 2)Problem: Identify if the number is composed of a repeating substring of any length (e.g.,
123123123
). • Naive Approach (Discarded): Iterating through all possible substring lengths and reconstructing the string results in O(L^2) complexity. This is inefficient for large inputs. • Optimal Approach (KMP Algorithm / LPS Array):Logic: We utilize the LPS (Longest Prefix which is also Suffix) array construction from the Knuth-Morris-Pratt algorithm. ◦ Mechanism: We compute the LPS array for the digit string. Let n be the string length and lastLps be the value of the last element in the LPS array. ◦ The length of the smallest repeating unit is calculated as:
unitLength = n - lastLps
. ◦ Validity Check: If lastLps > 0 and n is perfectly divisible by unitLength, the string is periodic. • Time Complexity: O(L). The LPS array construction performs a single linear pass over the digits. This reduces the problem from quadratic to linear time complexity per ID. https://github.com/karloti/aoc-2025-in-kotlin/blob/main/src/Day02.kt
e
I was trying to be cleverer but it ended up not working out (part1: maxSplit=2; part2: maxSplit=Int.MAX_VALUE)
n
Lots of premature optimization. Oh, well, it's reasonably quick for a Kotlin solution at 8ms (277us in "benchmark" mode). https://github.com/nbanman/pdx-puzzles/blob/main/kotlin/advent/src/main/kotlin/org/gristle/pdxpuzzles/advent/y2025/Y25D02.kt
👍 1
Anyone having trouble with the Slack client? It's been buggy on me more than once today. First time I've used it in a long time though.
j
Phil:
Regex for part 2 felt like the easy choice...
Regex never feels like the easy choice for me… But it seems helpful here.
m
At first I got a little scared by the fairly large numbers and thought iterating the ranges might not work. After I realized that the actual ranges where not that large it was pretty straight forward
1
a
sometimes the way the problem is stated also makes different solutions seem "obvious". for me, part 2 felt obvious to use the regex to check for variable length repetitions. but it never occurred to me to do that while doing part 1 until I reached part 2
m
@Max Thiele I usually just start writing with the assumption that I can brute force everything only really thinking about what datatype to use (set or treemap for instance). Only after my solution makes my computer fans go brrrr is when I decide: "aight I'll rethink it smartly"
k
Hi, @Neil Banman I took a deep dive into your solution today, and I have to admit—the constructive approach completely slipped my mind.
Y25D02.kt
In my implementation (), I relied on a linear scan with a filtering predicate. I iterated through the entire range, converting every integer to a string and checking for periodicity using KMP logic (). While this works, it scales linearly with the range size (O(N)).
Day02.kt isMultiPattern
Your strategy of mathematically constructing the "next valid number" (via and ) is far superior. Given the sparse distribution of numbers with repeating sub-patterns, calculating the jumps allows you to skip the vast majority of candidates. Your complexity effectively scales with the number of solutions rather than the input range.
getNextInvalid2 getNextByPortion
Excellent optimization!
c
Felt like a slightly harder than usual day 2 (though not too tough still) which suggests to me we might be ramping up a bit faster this year. Took me a minute to realise the rules, especially part 2. Was wondering why something like
123444567
wouldn't qualify as the 4s repeat in the middle before realising it had to be a repeating pattern that covered the whole string. We're lucky Kotlin has
chunked
as it trivialises the puzzle 🙂
😄 1
k
My Christmas tree 🎄
Copy code
duration =  1.691200ms (part2Fast) Complexity O(N)
duration = 75.588700ms ( part2 )
https://github.com/karloti/aoc-2025-in-kotlin/blob/main/src/Day02.kt
m
Short and readable. What do you think?
Copy code
fun main() {
    val ranges = readln().split(",").map { s -> s.split("-").map(String::toLong) }

    var part1 = 0L
    var part2 = 0L

    for (range in ranges) for (id in range[0]..range[1]) {
        val s = id.toString()
        for (l in (1..s.length / 2).reversed()) {
            val chunks = s.chunked(l)
            if (chunks.distinct().size == 1) {
                if (chunks.size == 2) part1 += id
                part2 += id
                break
            }
        }
    }

    println(part1)
    println(part2)
}
K 1
e
you can use
downTo
instead of
.reversed()
(or let it run in increasing chunk size, it doesn't matter)
🙌 1
m
^ No, it will be incorrect for part 1 unless reversed 🙂
true 1
d
used almost every function from the stdlib)
p
I think ~50us and ~35us is acceptable improvement over Regex
Copy code
Warming up 2 puzzles for 5s each for year 2025 day 2...
	ChunkGeneration warmed up with 73655 iterations
	Regex warmed up with 15 iterations
year 2025 day 2 part 1
	 ChunkGeneration took 50.747us 👑: 12850231731
	 Regex took 136.692622ms (2693.61x): 12850231731
year 2025 day 2 part 2
	 ChunkGeneration took 35.1us 👑: 24774350322
	 Regex took 196.730319ms (5604.85x): 24774350322
👍 1
mind blown 1
e
updated to avoid string manipulation and Set
sonic 1
e
Day 2 Part 1 can be just simple as this
And for part 2 it's just replacing
isInvalidId
with this: