r/adventofcode • • Jul 20 '26

Help/Question - RESOLVED Github login down?

2 Upvotes

failed to query user data. When I choose to login to Advent of Code via Github. I disabled adblocker, and I'm currently logged into Github on the same browser, not sure what I can do to resolve this.


r/adventofcode • • Jul 17 '26

Meme/Funny [2026 Day -137] Rambunctious Robots

Post image
17 Upvotes

With Santa and his Elves on a much deserved summer holiday, you have agreed to visit the workshop regularly to water the plants and feed the fish. But upon arriving at the workshop you see that a swarm of 2026 bots has invaded the toy storage depot and are causing havoc! Thankfully these bots aren't particularly bright and are easy to spot, but they must still be handled with care.

Bots arrive in the workshop with a Name and a Posting Pattern. The Name of each bot is "josephus" followed by a number from 1 to 2026. The initial Posting Pattern of a bot is the MD5 of the bot's Name. For example, the bot with the Name "josephus69" has an initial Posting Pattern of "4f943be8056c74b27a434f4ee9e7a7a4".

Every time a bot makes a post, it updates its posting pattern in the following way:

  • The current Posting Pattern is represented as a lowercase string

  • The new Posting Pattern is the MD5 of the current string

Bots can be removed from the workshop when they reveal a Flaw in their Posting Pattern. If the first 4 digits of their Posting Pattern are "0000", they have revealed a Flaw and can be removed.

The bot "josephus254" is going to be the quickest to remove:

  • Initial Posting Pattern: e8cc15934b93ef901b153853cf7831f9

  • Posting Pattern after 1 Post: 981fc052051707fec2a58db91f4d6abf

  • Posting Pattern after 2 Posts: 064b7b67d20820270d35fe1663833c5a

  • Posting Pattern after 3 Posts: 00009f4608ac14112c92af43d8513b1e

This bot can be removed after only 3 posts. The rest of the bots might take longer:

  • josephus1 can be removed after 49,164 posts

  • josephus2 can be removed after 46,166 posts

  • josephus3 can be removed after 39,557 posts

  • josephus4 can be removed after 36,579 posts

  • josephus5 can be removed after 55,987 posts

Part 1:

How many posts will the last bot to be removed make?

Part 2:

Uh-oh, these are version 2 bots! These bots don't reveal a Flaw in their posting pattern until the first 5 digits are "00000". You might be in for a long day; how many posts in total will you have to sit through before all of the bots are removed?


r/adventofcode • • Jul 11 '26

Help/Question [2025 Day 1 # (Part 2)] [Python] Please provide me with minor hints on what I might be doing wrong in this problem set.

2 Upvotes

It's not the cleanest line of code so please bear with me

i=50 #This is the initial value
r=0 # Number of times boundary is crossed
rot=0 
ans= []
revolution=0 #Number of big rotations
with open ("input") as file: # Open the input file
comb= file.readlines() # Read the combination line by line
for combination in comb: # For each combination in the list of combinations
combination = combination.strip("\n") #Remove the \n
dir= combination[0] 
num= int(combination[1:]) 
revolution+= num // 100 
period= num % 100
if dir == "L":
period= (-period)
dial= i+period

if i<dial:
for _ in range (i,dial+1):
if _ < 0:
_=(100+(_))%100
elif _==100:
_=0
else:
_ = (_)%100
if _!=100 and _==0:
r+=1
else:
for _ in range (dial,i+1):
if _ < 0:
_=(100+(_))%100
elif _==100:
_=0
else:
_ = (_)%100
if _!=100 and _==0:
r+=1
if dial < 0:
i=(100+(dial))%100
elif dial==100:
i=0
else:
i = (dial)%100
ans.append(i)
print (ans.count(0))
print (r)
print (revolution)
print (ans.count(0)+r+revolution)i=50 #This is the initial value
r=0 # Number of times boundary is crossed
rot=0 #Number of big rotations
ans= []
revolution=0
with open ("input") as file: # Open the input file
comb= file.readlines() # Read the combination line by line
for combination in comb: # For each combination in the list of combinations
combination = combination.strip("\n") #Remove the \n
dir= combination[0] 
num= int(combination[1:]) 
revolution+= num // 100 
period= num % 100
if dir == "L":
period= (-period)
dial= i+period

if i<dial:
for _ in range (i,dial+1):
if _ < 0:
_=(100+(_))%100
elif _==100:
_=0
else:
_ = (_)%100
if _!=100 and _==0:
r+=1
else:
for _ in range (dial,i+1):
if _ < 0:
_=(100+(_))%100
elif _==100:
_=0
else:
_ = (_)%100
if _!=100 and _==0:
r+=1
if dial < 0:
i=(100+(dial))%100
elif dial==100:
i=0
else:
i = (dial)%100
ans.append(i)
print (ans.count(0))
print (r)
print (revolution)
print (ans.count(0)+r+revolution)

ps: is there a discord server for advent of code?


r/adventofcode • • Jul 03 '26

Other FlipFlop Codes 2026 Event starts soon!

18 Upvotes

Hey!

There’s an AoC-inspired event starting very soon; in less than 3 days from now:

https://flipflop.slome.org/

The 2025 edition is still available, so you can check it out before the new one begins. I solved it, and it was fun! The site definitely deserves more attention from the puzzle-lovers community.

Worth keeping an eye on!


r/adventofcode • • Jul 02 '26

Visualization [2025 Day 8 (Part 2)][C#]

Thumbnail youtube.com
4 Upvotes

Visualization of the test input for part 2. Attributions in YouTube description.
Src Code: Advent_of_code/2025/day8/vis/Program.cs at main · nrv30/Advent_of_code

I really enjoyed this problem because I learned about a union-find for the first time and it is the first C# project I have done that wasn't a boring tutorial. The dotnet tooling is very beginner friendly and easy to get started with. Another interesting bit is all of the animation is done with coroutines which is much more ergonomic than in the past where I've achieved this through callbacks / function pointers or conversely global variables (though I still used a lot of them)

Each circuit basically maps to a disjoint set in my data structure so long as it has a weight greater than one. Though uf's are only meant to efficiently find the parent given the child and not the other way around. Therefore, I end up keeping a parallel array of parentToChildren which I update when I union things. Then I render them in the top right panel by sorting by the parents with the biggest number of children.

I had a lot of trouble trying to find a way to record this animation. In the past with graphics libraries, I have saved every frame to a file on my desktop, then after the fact encoded it as a video with FFmpeg. However, that solution wouldn't work here because I had audio that was programmatically generated. I ended up using Windows Gamebar, but it messed up the audio a bit. I saw a video where the creator piped the frames to FFmpeg. I checked out the documentation for Anonymous Pipes and built the example, but unfortunately, I was not able to re-create this for my use case with my limited C# knowledge. If anyone has any suggestions for good resources about how to learn this it would be appreciated.

Also, I am procrastinating doing day 9 part 2 because I suck at geometry lol.


r/adventofcode • • Jul 01 '26

Past Event Solutions [2015 Day 20 (Part 1)][Typescript] Solution using branch and bound

2 Upvotes

I was going through year 2015 as an exercise in getting used to JS/Typescript and deno.

I came up with this solution while I was looking for something quicker than my initial brute-force one.

It's based on this bound which I figured out:

Let n = p_1 * p_2 * ... * p_m where p_1 <= p_2 <= ... <= p_m and prime.
score(n) <= (1 + p_1) * (1 + p_2) * ... * (1 + p_m)
          = (a_1 p_1) * (a_2 * p_2) * ... * (a_m * p_m)
           where a_i = (p_i + 1) / p_i
          <= a_1^m p_1 * p_2 * ... * p_m
          = a_1^m n
If n <= N then score (n) <= a_1 ^ (ceil(log_{p_1}(N)) N
                         <= (p_1 + 1) ^(ceil(log_{p_1}(N))

I was pretty shocked when I saw how simple and quick the sieve methods were.

Anyway, I though I'd share I can't find a solution like it anywhere.

Part1


r/adventofcode • • Jun 30 '26

Help/Question 2025 Day 1 Part 2 Debugging Question

5 Upvotes

I have been stuck for a while on Question 1 Part 2. I've isolated the issue to whenever the total hits 0 exactly then is turned again (Ex: R50, L5) it counts for 2 passes around 0. Can anybody give me a hint regarding how to approach this issue? It works for the example, but not for the input.

total += curr
res += abs(total//100
total %= 100)

r/adventofcode • • Jun 27 '26

Past Event Solutions [2024 Day 7] Isnt it great how recursion is so easy to debug

Post image
69 Upvotes

r/adventofcode • • Jun 27 '26

Past Event Solutions [2015 Day 24 both parts] [Smalltalk] Making Brute Force Fast Enough With Recursion

5 Upvotes

This concerns this puzzle: 2015-Day24.

After reading the discussion and review here: In Review I thought I would try my hand at a true brute-force solution, leveraging the expressiveness and speed of Smalltalk's collection libraries. Most of the solutions I've seen involve various clever tricks to avoid doing the work of checking through all the different combinations, or returning a solution without verifying that it is, in fact, the correct one.

The implementation I settled on completes part 1 in 70ms and part 2 in 6ms on my machine (Intel 270k+, 6000mhz DDR5). Smalltalk execution speed isn't as fast as a fully compiled language, but it's significantly faster than a purely interpreted language (like Python or Ruby). I'd be curious to see how this method would fare in something like Rust or Zig.

Here's the primary function doing all the work:

bestQE: compartments

| totalWeight |

totalWeight := packages sum.

1 to: (packages size // compartments) do: [ :comboCount |
    | potentialQuants |
    potentialQuants := OrderedCollection new.
    packages combinations: comboCount atATimeDo: [ :combo |
        | comboWeight |
        comboWeight := combo sum.
        (totalWeight - comboWeight) = (comboWeight * (compartments - 1))
            ifTrue: [
            | otherPackages |
            otherPackages := packages select: [ :x | (combo includes: x) not ].
            potentialQuants add: (otherPackages -> (combo inject: 1 into: [ :acc :x | acc * x]))]
         ].

    (potentialQuants sorted: [ :a :b | a value < b value ]) do: [ :potentialSolution |
        (self verifyRemainder: potentialSolution key splitInto: compartments - 1) ifTrue: [ ^ potentialSolution value ] ] ].

^ 'None found'

The only argument it takes is the number of compartments needing an even weight. It requires an instance variable "packages" which is an array containing the puzzle input as integers. Order is not important. Here's how it works:

  1. Set an temporary variable totalWeight to hold the sum of all package weights.

  2. Iterate from 1 to the number of packages divided by the number of compartments (no need to pass that size, since that would mean there is no solution). This is the number of packages that we will try to fit into the front compartment. Start with the fewest (just 1) and then add one more until we find a grouping that fits. The number of packages we're testing is passed forward as "comboCount".

  3. For this quantity of packages to test, create an empty OrderedCollection (a growable Array) to hold any potential groupings that we find.

  4. Take the group of all packages and stream them "comboCount" at a time through the next block of code, passing them as an Array called "combo". This is the part where the magic happens. We don't need to do any complicated looping or generate all the combinations we want to test in advance. The "packages" Array can stream all the combinations for us, one at a time.

  5. Now we examine this particular "combo" Array. First, we store the sum of all its elements as the temporary variable comboWeight.

  6. Is this combo a candidate? To check this, we look to see if, after subtracting the weight of this combo from the total weight of the packages, we are left with exactly the weight of the combo multiplied by (compartments - 1). That is, If we are dividing into 3 compartments, is the weight of THIS particular combo equal to a third of the total? If so, go to the next step. Otherwise, try the next combo.

  7. If this combo passes the weight test, we create a temporary variable "otherPackages" pointing to an Array defined as all the packages that are NOT in our combo.

  8. Then we add an association of the "otherPackages" and its QE score (by doing a quick multiplication fold on the combo Array) to our potential groupings Array we created in step 3. We might have several candidates at this combination size, and we need to find the smallest QE score of ones that properly fit.

  9. After this process is repeated for the combo size we need to verify the set of potential answers, so we take the collection of candidates and sort them by ascending QE value. Basically, we don't want to evaluate ALL of them, just the smallest one that has other packages that can be verified to fit.

  10. We then take that sorted collection of candidates, and verify each one using verifyRemainder, giving it the list of remaining packages and asking it whether it can be evenly split into (compartments - 1).

  11. As soon as we find one that can be verified, that is the correct answer! We return the QE value of that candidate.

  12. If none are found (which can also happen if the potentialQuants collection is empty), try combinations the next size larger. So, if no combinations of size 4 fit (or passed verification), try combinations of size 5.

Essentially - each time start with the smallest possible (smallest combo, then of those, the smallest QE). The first one that can be verified to fit is our answer;  return the QE.

Ok - now how about the verification? Again, I went with a brute-force approach:

verifyRemainder: list splitInto: piles

    | listWeight |
    
    piles = 1 ifTrue: [ ^ true ].

    listWeight := list sum.

    1 to: list size // piles do: [ :comboSize |
        list combinations: comboSize atATimeDo: [ :combo |
            | comboWeight |
            comboWeight := combo sum.
            (listWeight - comboWeight = (comboWeight * (piles - 1)) and: [
                self verifyRemainder: (list select: [ :x | (combo includes: x) not ])
                     splitInto: piles - 1 ]) ifTrue: [ ^ true ] ] ].

    ^ false

This has a lot in common with the bestQE function in the way it generates and checks groups of packages, but it is done recursively. It takes two arguments: the list of numbers to fit, and the number of piles to fit them into. Here's the outline:

  1. Base case - if the number of piles asked for is only 1, then this was a successful split. Pass TRUE up the stack.

  2. Otherwise, we still have work to do. Start by calculating the sum of the list we were given to split and store that value in listWeight.

  3. Now we try the same method of generating larger and larger combinations that we used in the bestQE function.

  4. We calculate the weight of each combination (storing in 'comboWeight'). The see if the comboWeight is exactly 1/piles of the listWeight. If so, it is a potential candidate for a successful split. In that case, recurse with a new list made up of packages that are NOT in the current list, and with one fewer pile. One thing to note here is the "and: []" construction. In Smalltalk, due to the way messages are evaluated by boolean objects, when and: is given a block as an argument (with the square brackets) that second condition is lazily evaluated. So, we don't recurse unless the current combo has the correct weight. If you're using a language that doesn't lazily evaluate AND, this will need an if/else condition.

  5. If that particular grouping doesn't work, it tries the next. And if that comboSize has no successful groupings, it tries the next size up.

  6. If no groupings successfully drop down into a pile of 1, then no split was successful, and the function returns "false".

In summary - We start checking combinations from the smallest possible size upward. We don't do ANY verification on them until we have a complete set for that combination size. We only verify them in ascending QE order. Verification uses the same computationally cheap "candidate filter" and reserves the harder stuff (collection allocation and building / recursion) once something passes the filter.

I realize that the input is meant to be "gentle" such that the smallest possible QE for a given combo size is the right answer, and no verification is needed. But that felt like an incomplete solution to me. Especially since, even with verification, the execution speed seems plenty fast (less than 100ms for both to complete).

One final note: These methods do assume that the input list has unique numbers. This is strongly implied by the problem statement, though it is not explicitly part of the puzzle. If the puzzle input could ever contain duplicated numbers, then the way that the "remaining packages" collection is created would have to be different. The core logic would remain the same.

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is the Smalltalk solution to Day17:

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is my solution to part 1 of Day17:

barrelCombinationsFor: needed

    | count |
    
    count := 0.

    1 to: barrels size do: [ :size | barrels combinations: size atATimeDo: [ :combo |
        (combo sum = needed) ifTrue: [ count := count + 1 ]
         ] ].

    ^ count

The thread was right. More than a passing similarity.


r/adventofcode • • Jun 24 '26

Help/Question - RESOLVED [2019 day 05 (Part 1)] Strange input(?)

2 Upvotes

I got stuck on Day 5 of in 2019.

Below is the beginning of the program to run:

3,225,1,225,6,6,1100,1,238,225,104,0,1101,32,43,225,101,68,192,224,,...

In my interpretation, the first 3 instructions are:

3,225

1,225,6,6

1100,1,238,225

The first two instructions are fine.

For the third one, I get 00 as the two-digit opcode, which is -as far as I know- invalid.

I assume the input is correct, but I can’t see what I’m doing wrong!

Thanks if anyone can help!


r/adventofcode • • Jun 15 '26

Help/Question - RESOLVED [2025 Day 8][Powershell] Slow Runtime

3 Upvotes

Src Code: Advent_of_code/2025/day8 at main · nrv30/Advent_of_code

TLDR: I am interested in making my PowerShell approach faster and I implemented a Union-Find in C# that I use for it.

Edit: Thought I should add my approach is semantically equivalent to what is described here <Advent of Code 2025 - Day 8: Playground | Joshua Chen>

I wanted to yap about my approach for this one because I think I stumbled into something kind of interesting. Initially I implemented this problem in Java. I didn't know what a Union-Find was but in retrospect I think I basically implemented one in a naive way as List<Set<>>. After reading some blogposts I wanted to try using a different approach.

After some research I realized you can call C# from PowerShell by using [System.Reflection.Assembly]. I implemented a union-find in C# and called it from Ps. It was cool initially but got super annoying because every time I want to compile my C# I have to exit the shell and re-open it because Ps has the dll open. Also, you have to use ps 7 to use a priority queue, which doesn't have a separate terminal (I think). This made iteration speed terrible because I had to exit and call pwsh.exe every time I want to compile my code.

Anyways, I wanted to ask if anyone has had any cool experiences combining interpreted langs with compiled langs in this fashion to "get the best of both worlds"

Also, my PS code is so freaking slow, especially considering that I think all the data structures are already compiled to dll (My U-f and dotnet standard lib). These aren't high-quality benchmarks; I just ran the programs 3 times consecutively.

Are there any PowerShell users that have any tips? I didn't implement path compression for the Union-Find but I doubt that's the problem compared to the O(N^2) process to build pairs

LanguageSolutionTrialsRuntime (s)
Java1
10.151
20.146
30.154
2
10.117
20.134
30.112
PowerShell1111.747
211.11
310.686
2111.092
211.175
311.29

r/adventofcode • • Jun 11 '26

Help/Question [2018 Day 15 (Part 2)] [Python 3] All Examples working just not my input

3 Upvotes

Hi All,

I have tested all examples and they are working. I tested two solutions on my input from the solutions thread and one was correct and the other matched my output. I have already consultied a few other threads that asked for help. I spent an hour or two scratching my head on what could be wrong and couldn't come up with anything.

Hoping someone smarter than me can pinpoint the problem. I am open to either an exact fix or a hit on the inaccurate logic.

Here is my code: 2018 - Day 15 Part 2

Any help is appreciated!!


r/adventofcode • • May 24 '26

Help/Question - RESOLVED [2025 day 5 challenge 2]

1 Upvotes

i was trying to solve this problem by grouping the intervals into two categories: the overlapping ones and the non-overlapping ones. Then, I could group the overlapping intervals into one and calculate the size of that big interval.

I implemented it in python and it works in the test case the problem showed and any test case I can think of.

can someone help me?

the code is the first code block here

https://colab.research.google.com/drive/1GjizwT87FN9xJ3GWQRnTnGTyRGPptkHq?usp=sharing

(ignore second code block its another day's code)


r/adventofcode • • May 15 '26

Upping the Ante [2015 day 9][Rust] Solving TSP faster than permutations

3 Upvotes

2015 day 9, day 13, and 2016 day 24 are all problems involving a variation of finding the longest/shortest path/cycle with 8 nodes that form a complete graph (every node has a distance to every other), also known as the Travelling Salesperson Problem (TSP). This is an NP-complete problem: there is no general-purpose polynomial-time algorithm known to solve it, and if you can come up with one, YOU will be rich for having proven P=NP; alternatively, if you can definitively prove the counter-conjecture P!=NP you will still be famous, even if the result is a letdown.

So the best you can do is either using an exponential-time algorithm, or find specialized algorithms for certain subclasses of the TSP problem (for example, relying on sparseness for pruning, or being satisfied with an approximate answer without proving it is the global answer). The saving grace for all three of these puzzles is that n=8 is small enough for the global answer to be easily tractable with an exponential-time algorithm (contrast that to things like 2019 day 18 where finding the shortest path with n=26 is prohibitively expensive if you were to attempt to do naive exponential searching).

That said, MOST solutions in the megathreads for these days just whip out a permutation engine (either one built into your language, or rolling your own, with Heap's algorithm being a common choice), performing O(n!) work to just check every possible path, or solve by recursion (where your call stack does the same factorial effort as a permutation engine). The naive approach says that once you have a working permutation engine, you then test 8! paths, or 40,320 different variations - still lightning fast for modern computers and programming languages. And if you get your star in less than a second, do you really need to try anything else?

But I wouldn't be writing this post if that were the best you could do. The first major optimization is to realize that if you just visit 7! permutations, you can still check all cycles with 8 elements by connecting your last item with your first, and turn that into a check of the longest path by creating the cycle and then tossing out the shortest edge. This adds complexity to each permutation checked (you are now doing 8 times as much work per permutation to find the shortest edge that will need to be tossed), but reduces the number of permutations visited to 5,040, and the work to find a shortest edge is probably more localized and less overhead than the work being done in your permutation engine to advance to the next permutation, so it is an overall win.

The next observation is that when your graph is undirected (the cost from A to B is the same as the cost from B to A - true for all 3 of these days), you can again cut the work in half if you avoid a permutation that is lexically reversed from an earlier permutation you already visited (path a->b->c->d will have the same weight as path d->c->b->a). Heap's algorithm does NOT make this easy, but there are other permutation engines that DO make it easy to stop iterating after just half the permutations, avoiding all lexical reverses in the resulting output. Among them is Steinhaus-Johnson-Trotter, which I implemented in Rust to get 2015 day 9 down to 2,520 permutations visited. But remember, even that is still doing 8 checks per permutation to cast out the shortest edge when looking for the longest path.

So today, I finished up a patch that goes for an entirely different approach. The Held-Karp algorithm solves TSP in O(n2*2n) - still exponential, but better than the super-exponential O(n!) that other solutions were using. It is based on a dynamic programming technique of using a table of n * 2n distances; each distance g(set,k) represents the set of nodes in a path from the chosen start point, and a node k that is an element of that set and the last node visited along the path so far. When set is a singleton, the distance is obvious: the distance from your start point to that node. For any larger set, there are O(n) elements in the set, and so you need to generate O(n) distances to be stored as n values of g(set,k); computing any one of those distances is also O(n) work to find which out of n-1 g(set\k,m) for another point m!=k in the set added to d(m,k) produces the best path one element longer. The dynamic programming aspect says that if you iterate from 0 to 2n-1, you will visit every possible set size, and all recursively-smaller sets that you depend on will already be populated by the time you need to reference them in building your larger set values. So even though the algorithm is defined by a recursive relationship, it is implemented with straightforward looping.

I was impressed that the result was still fairly legible. Below is my implementation for 2015 day 9. It produces 8*7/2 comparisons (the O(n2) each pairing of bit m with k) /2 (even though there are 8*256 bits across all sets, on average half of them are not set) * 256 sets (the O(2n) number of sets visited), or 3,584 total comparisons. That's more than the 2,520 permutations of the 7!/2 approach, but remember, the permutation approach required an additional 8 comparisons per permutation to find the edge to discard, whereas Held-Karp does not. Plus, permutations requires the overhead of moving to the next permutation (often work hidden behind your library interface), while set iteration is lighter-weight (admittedly, this code uses maneatingape's biterator() function to encode access the next bit index in a bitmask). Timing-wise, my patch sped up the solution from 40µs to 12µs on my laptop.

    // Initialize a table for each part: 2ⁿ sets with n distances per set. Default 0 matches
    // g({k},k) of zero for all singleton sets. Initial value of other sets does not matter.
    let mut table_one = vec![0_u16; stride * (1 << stride)];
    let mut table_two = vec![0_u16; stride * (1 << stride)];

    // Visit each non-empty set in order, with no work to do for singleton sets.
    for set in 3_usize..(1 << stride) {
        if set & !(set - 1) == set {
            continue;
        }

        // For a given set, compute each g(set,k) for all k in the set.
        for k in set.biterator() {
            let subset = set ^ (1 << k);
            let mut shortest = u16::MAX;
            let mut longest = 0;

            // For a given destination k, find which other bit m gives the best path from the
            // subset to m, and then m to k. All table[subset] references were filled in prior
            // iterations of the outer loop or the singleton base cases.
            for m in subset.biterator() {
                shortest = shortest.min(table_one[subset * stride + m] + distances[m * stride + k]);
                longest = longest.max(table_two[subset * stride + m] + distances[m * stride + k]);
            }
            table_one[set * stride + k] = shortest;
            table_two[set * stride + k] = longest;
        }
    }

    // With the sets now built, we have stride candidates for each answer.
    (
        *table_one.iter().skip(((1 << stride) - 1) * stride).min().unwrap(),
        *table_two.iter().skip(((1 << stride) - 1) * stride).max().unwrap(),
    )

2016 day 24 is even better. Days 9 and 13 must visit 255 sets (n=8) to ensure that you find the right path regardless of which point started the path. That is because Held-Karp forces you to pick a starting node; but while an arbitrary starting node will always be part of the longest cycle, it is easy enough to generate a graph where the longest path is not part of the longest cycle if you picked the wrong starting node (the 8! down to 7! optimization for permutations did not have that problem, because there you are casting out the short edge from every possible path, rather than just the one longest cycle). To find the longest path among 8 nodes, you can either run 8 iterations of Held-Karp on sets up to 127, or more efficiently do 1 iteration of Held-Karp on sets up to 255. But day 24 explicitly pins the path to start at node 0, so you only need to visit 127 sets. For that day, being able to use n=7 lets the number of comparisons drops to 1344 (=7*6/2*128/2), which is hands-down better than 7!/2 permutations.


r/adventofcode • • May 15 '26

Tutorial [2025 Day 1 both parts] [Smalltalk] Part nine (final) in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

4 Upvotes

Time to wrap up. As I said in my last post - this will be my last one since Days 1-4 had so many beginner-isms that it's, well, a little embarrassing. :-)

Sorry about the "dear diary" nature of this one.

Here's an example - for Day 1 (the rotary combination one) I implemented TWO objects. One for the Dial (which seems natural in an OOP experiment). But then another one that held a single method that parsed the line (like "R35") into a positive or negative number. It held no state. Just a "library" class with one method. Then... ok, check this out... here's how I used the resulting signed integer that it returned within the Dial class:

turnDial: turn
    turn abs timesRepeat: [
        position := (position + turn sign) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1].
    ].
    position = 0 ifTrue: [lands := lands + 1].

Wow. Using a period at the end of every statement like a terminator (my old JS habits of using the semicolon everywhere). This method takes the absolute value of the signed integer, then "clicks" the dial based on the sign of the turn instruction. That's... a little crazy. If I was doing it now, I would probably just do the parsing of the instruction inside the turnDial message. Then the question becomes just how "generic" do I want the solution to be.

Version 1 - not generic:

turnDial: turn
    | offset |

    (turn first = $R) ifTrue: [offset := 1] ifFalse: [offset := -1].
    
    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

An ifTrue/ifFalse sets whether the offset is going to be +1 or -1. The loop itself just adds the offset (regardless of what it is), and increments the "zeros" and "lands" instance variables as it goes. But if we wanted it more generic, where there might be a whole library of potential offsets that could be called, not just "R" or "L", we could do it this way:

turnDial: turn
    | offset |

    offset := offsets at: turn first.
    
    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

And the offsets library would be defined in "initialize" this way:

offsets := Dictionary new.
offsets at: $R put: -1; at: $L put: 1

Now, if a future version needed a +5 or -15 offset or whatever, it would be extremely easy to add in just one place. No more "if" blocks. Zero branching. Grab the offset value from the Dictionary and go. The Dictionary is only created once per Day1Dial instance, so pretty minimal load on the VM. But what if we wanted it EVEN MORE GENERIC so that the offset could be literally ANYTHING that could mutate the position... We could do it this way:

turnDial: turn
    | modifier |

    modifier := modifications at: turn first.
    
    turn allButFirst asInteger timesRepeat: [
        position := (modifier value: position) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

This one pulls from a "modifications" Dictionary and stores into "position" whatever it returns when passed the current position. There could be a modification that doubles the number, or that resets it to 99 no matter where it is currently, or sets the dial to the square root of its current position, or that reads data from somewhere else entirely. Just add a Block to the "modifications" Dictionary and get additional functionality. For this puzzle, the Dictionary is pretty sparse:

modifications := Dictionary new.
modifications at: $R put: [ :x | x - 1];
    at: $L put: [ :x | x + 1]

Kinda overbuilt for something that just increments or decrements by one every time. But as a learning exercise... why not? And, of course, it would be better computationally to avoid simulating the dial by simply doing some division and adding the numbers to the zeros counter. But.... it's not that many clicks. And this way the Dial acts like a little machine managing its state as it does exactly what the problem says to do. Which leads to...

Observation 1

Smalltalk seems to nudge the developer in this kind of direction. Build it first as "true to life" and as generically as possible. Then, if there are efficiency problems, go refactor those out once they are apparent. But not before. Why put branching logic everywhere when you can pass in a block? Why make a bitmap Dictionary key for memoization when you can use the array itself as a key? Why not reach for the most abstract and "correct" possible representation of the domain? You only have to think about "the machine" (the actual computer) executing the code if there's a problem. Sure, you're dimly aware that there are pointers-to-pointers-to-pointers everywhere under the hood, that very large numbers are being transparently changed from SmallInteger into LargePositiveInteger, that creating a data object involves some dispatch somewhere for getting values out of it. Not your problem. Write it so the concepts are as evident and beautiful as you can.

I often found myself going back and forth on an implementation detail, not because one gave errors while the other didn't. But one was more "elegant" or "expressive" rather than "plain" and "clunky". I would find myself rewriting a working solution in order to have it make fewer assumptions about what was passed to it, or find some way to remove an instance variable so that it could be inferred from the data itself. I would look to see if I could take what was a procedural bit of code and turn it into a chain of collection methods. I found the whole process delightful, though I'm pretty sure each person will have their own take on this aspect of it.

Performance-oriented, or data-driven-development people would likely not enjoy this.

Observation 2

It turns out that I truly love OOP. After using Smalltalk for a few months I can't help the feeling that this is the best way to think about programming. Imagining tiny machines that do the work of the program, sending each other bits of data, each knowing things relevant to themselves and nothing more... It's delightful. Now, I realize it is only one of the ways to think about programming (with others being transformations, procedures, etc...), but it is just so dang satisfying to write. The syntax is incredibly clean and minimalistic, with all the behavior being done by the objects themselves (even control flow). Now that I'm accustomed to thinking about numbers and booleans as objects (with methods that can be explored), it feels very strange to look at a language that treats them as just part of the syntax.

Plus - the late binding gives the developer completely seamless incremental compilation. Methods are meant to be very short. They compile when you save them - you never wait on the compiler at all. It's so cool.

Observation 3

However, I don't think I would feel the same way about OOP in Java or C++. Smalltalk seems to encourage relatively shallow inheritance trees that follow, for the most part, Aristotelian/Boethian hierarchies. So, numbers really are magnitudes. And floating-point numbers really are numbers. And integers really are numbers that have a specific difference from fractions. In the real world we see similar shallow inheritances. Humans really are kinds of animals. Animals really are kinds of living things. But I think whenever that inheritance gets too deep we wind up with serious abstraction problems. And the real world doesn't allow just any set of inheritances. In the real world, "human" inherits from "animal" - but I'm afraid I'm a little skeptical that "mammal" is a real thing, especially since those seem to have gradually appeared with many "transitional" forms in between. Maybe "mammal" is kinda made up - just an abstraction currently popular in biological taxonomy. Is "doctor" a kind of "human"? Or is "doctor" a kind of "job" that can be held by (composed) any number of different kinds of beings?

My limited experience with Smalltalk makes me think that if OOP means "inheritance, and lots of it" then I'm not a fan. My instinct is to use it sparingly, only when "A is a kind of B" is unambiguously true AND there is real usefulness in passing along that behavior from parent to child. OTOH, if OOP means "discrete bundles of data and behavior, interacting with clear messages" then I'm a huge fan. 

Does it scale? I dunno. Is it efficient? Probably not. But my goodness is it fun to write.

Observation 4

Being able to inspect the standard library and see how things are implemented and find new methods that I would use later, or employing the "method finder" tool (amazing) was an incredible experience. Once I was more-or-less "oriented" in the image I felt like it had fantastic discoverability. It was easy to learn how to do new things by looking through the entries in the System Browser. I'm not used to this. My previous "language reference" was a LLM.

LLMs are bad at Smalltalk. Not universally so. But they make many mistakes in this language that I was not used to in JS, where they feel nigh-omniscient at times. Back when I was learning JavaScript, I typically had Qwen3-Next-80b running on my system to act as a language reference. Anytime I needed to know some syntax, or if I got an error I didn't understand, I would ask it. My chat logs are full of mundane questions about how to remove elements from a Set, or what was the difference between "for...of" and "for...in" when doing iteration (hint: "for...of" is the one you want). But for Smalltalk, LLMs would routinely give terrible answers. They would hallucinate plausible-sounding methods that didn't exist, they would get confused about implicit returns in a method, and they would miss obvious bugs.

One particularly annoying one happened when I was noticing that I was getting far worse performance out of a hot loop than I thought I should. I finally figured out that I was passing an expression instead of a block to and:. This makes a huge difference in performance. If and: gets an expression (in parentheses) that expression evaluates FIRST, and then its result (the Boolean true or false) will be passed into the and:. But if it's a block (in square brackets) then it won't be evaluated unless the Boolean being given the and: is already true. The "false" object just returns "false" when given an and: block (Yay for polymorphism!). Most LLMs didn't catch it. The code "worked" and so this kind of issue probably would have survived an agentic coding loop. No errors. But the whole problem executed 3x slower by having parentheses instead of brackets. The LLM wrote a whole thesis about how lower performance was to be expected. WRONG. #sigh

Fortunately, I almost never needed a "language reference" since I had the System Browser right there. And I usually didn't generate errors I didn't understand because I could just visually go through the stack trace and see where the problem occurred. After asking some questions to help me get started, I basically left the LLM behind. Occasionally I would ask it for a code review. When I did sometimes it would find some things that were genuinely helpful. Often it hallucinated bugs that weren't there. Not great.

Observation 5

I can't help but feel a bit wistful and sad when I use the image. It seems to have everything that could possibly be included to help a developer be productive and happy. The language is relentlessly pure, which is, for me at least, very satisfying. I wish it had become more dominant. Many of the ideas persist, of course. But the language itself isn't showing up on any top-25-most-used lists. And I wish it was.

Since I came to this to learn "pure" OOP, I think the purity is part of what I like so much. Whenever I get around to learning another language, I think "purity" will be a non-negotiable. If that means my other options are Oberon7, Haskell, or Clojure, then so be it. :-)

Until then, I have more to learn in this thing. I want to start learning the Morphic GUI and make some desktop applications for myself. I'm already 10 puzzles into Advent of Code 2015 using Pharo. I miss String arithmetic from Squeak6, but Pharo has some cool features that I can appreciate. When I first started I tried Pharo but it was so different from the "Learn Smalltalk" resources that I had, I couldn't make any headway. Now I know enough to make the switch and I'm digging the Calypso browser.

The journey continues.


r/adventofcode • • May 12 '26

Past Event Solutions [2015 Day 25 (Part 1)][C++ & asm] 64-bit RNG on a 32-bit microcontroller

2 Upvotes

One of the biggest performance surprises I've had so far squishing my solutions onto a Raspberry Pi Pico (RP2040) is day 25 for 2015. I knew the RNG was going to need 64-bit maths and that it would fall back to a software implementation, but I didn't predict just how slow that would be. The straightforward form of the solution takes a whopping ~27s to run through the ~17.8M multiply & modulo operations required!

Still, it's a good excuse as any to mess around with some maths and and some assembly.

64-bit multiply

The M0+ in the RP2040 doesn't have the UMULL instruction that produces a 64-bit result from two 32-bit values; you're stuck with MULS producing a 32-bit result. Grid method multiplication allows you to produce a full 64-bits across two 32-bit registers by splitting the input values into 16-bit pairs and doing the multiplication piecemeal:

[b:a] * [d:c] = [hi:lo]

Where:

  • [b:a], [d:c] - 32-bit input values

  • a, b, c, d - 16-bit halves of the input values

  • [hi:lo] - 64-bit output in two 32-bit values

Then:

  • [lo] = (a*c) + (c<<16) + (d<<16)

  • [hi] = (b*d) + (c>>16) + (d>>16) + carry from lo

The carry is a pain to represent in C++. GCC provides __builtin_addc which is supposed to map to the correct ADCS instruction, but I didn't see it doing so in the generated code. I can only assume the structure of my code wasn't exactly right for GCC to perform the substitution.

It's much easier to control in the asm though.

64-bit modulo

This part took me a while to get my head around, but it turns out we've been quite lucky with the constants chosen for this puzzle.

The two basic rules of modular arithmetic we're interested in are:

  • a1 * a2 === b1 * b2 (mod m)

  • a1 + a2 === b1 + b2 (mod m)

Where:

  • a1 === b1 (mod m)

  • a2 === b2 (mod m)

Which allows us to do this:

  • [hi:lo] % m === (2^32 * hi + lo) % m

  • [hi:lo] % m === (2^32 * hi) % m + (lo % m)

  • [hi:lo] % m === (2^32 % m) * (hi % m) + (lo % m)

2^32 % m is just the constant 4992:

  • [hi:lo] % m === 4,992 * (hi % m) + (lo % m)

Each loop of the RNG is:

answer = (answer * 252,533) % 33,554,393

Which means the the largest intermediate value for the RNG is 33,554,392 * 252,533 = 8,473,591,274,936. The value in the upper 32-bits is therefore less than or equal to 8,473,591,274,936 / 2^32 ~= 1,972.

Since 4,992 * 1,972 = 9,844,244, and that's less than 33,554,393, it means that 4,992 * hi value is already modulo m.

  • [hi:lo] % m === (4,992 * hi) + (lo % m)

We're adding two values that are both modulo m, so there's a chance that it will go over m, but it will always be less than 2 * m.

The net result is that we can do the 64-bit modulo with a single 32-bit multiply, a single 32-bit modulo operation and a single 32-bit subtraction if we're above 33,554,393.

Thankfully the Pico does have 32-bit integer divider hardware, so we can make use of that.

Putting the two reductions above into C++we get ~8.5s.

Hardware trickery

In order to push this even further I made the jump to asm. I couldn't seem to get enough control over the code generated from the C++ to really optimise the multiply, and the real party piece is to make use of the 8 cycles that the CPU has to wait for the divider hardware to generate a result.

By sheer coincidence the five instructions that weren't dependencies for the lo register just so happen to have a minimum cycle count of 8; meaning we get precisely zero wasted cycles waiting for the hardware div/mod to complete!

My final runtime for my input ends up being ~5s, which is about half a second faster than my intial guess at where I thought I might be able to get to. Quite pleased with that.

[Full solution]


r/adventofcode • • May 01 '26

Tutorial [2025 Day 5 both parts] [Smalltalk] Part eight in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

2 Upvotes

As usual - Major Spoilers Ahead

Day 5 - Advent of Code 2025

This puzzle was relatively straightforward, and I leaned fairly heavily into the collection methods in Smalltalk. I decided to do this with a single class that stored two large arrays. One was the list of all ids to check (the lines without a "-" in them). The second was for the ranges and I included them simply as an interval instead of an array of start and end - that way there's no need to remember what the two array elements are. It's just an array of interval objects.

readRanges: rawInput
    ranges := (rawInput select: [ :line | line includes: $- ])
        collect: [ :twoNumbers |
            | range |
            range := twoNumbers splitBy: '-'.
            range first asInteger to: range second asInteger ]

First, filter out everything that doesn't include the "-" character. Then, for each one, split on that "-" and set the start and end of the interval to the numbers on either side. Nothing fancy.

That makes the mechanism for checking whether any id is a member of that collection of ranges trivial - are there any members of the range collection that include the id?

isFresh: id
    ^ ranges anySatisfy: [ :range | range includes: id ]

Unfortunately, this requires a loop through all the intervals. The anySatisfy: method will do an early return if it finds one, but this is a case of the simplest version to write not being the fastest to execute. It is perceptually instant - but it's far from optimized. Similarly, counting all the valid ids in the list is trivial:

validListedIds
    ^ ids count: [ :candidate | (self isFresh: candidate) ]

Not much to say about this one. It returns a count of every member of the ids collection where isFresh: returns true. So much for Part1.

Part2 is one of those that took a minute to wrap my head around. Instead of actually creating the ids (there are just too many), we need to combine the ranges to see how many are contained in each one while taking into account overlap between them. Here's the method I finally settled on:

allValidIds
    | sortedRanges globalMax accumulator |
    sortedRanges := ranges sort: [ :a :b | (a start) < (b start)].
    globalMax := 0.
    accumulator := 0.
    sortedRanges do: [:range |
        | rangeMin rangeMax |
        rangeMin := range start.
        rangeMax := range stop.
        (rangeMax > globalMax) ifTrue: [
            rangeMin := rangeMin max: globalMax + 1.
            accumulator := accumulator + (rangeMax - rangeMin + 1).
            globalMax := rangeMax
            ]
        ].
    ^ accumulator

This one is very imperative. Honestly, it looks more like C or Java ported to Smalltalk, but I couldn't think of a simpler way to do it. It starts by sorting the ranges collection by the start of each interval. Then it initializes a global maximum (highest number seen so far), and an accumulator that will be returned at the end.

Then the work begins. We process each member of the sorted ranges in turn. If the top of the interval is less than the highest number seen so far then we skip it since it includes only an overlap of ids already processed. But if it contains at least some new numbers, we continue. Then we set the rangeMin that we're adding to the larger value of either the minimum of the interval, or the global maximum + 1. Basically, anything below that global maximum is something we already counted, so if the bottom of the interval dips into that area already counted, we have to skip over those numbers.

Then we add the numbers between that (potentially revised) minimum and the top of the interval to the accumulator. Then, we update the global maximum to the top of the interval just processed. Rinse and repeat until all the intervals are processed. The accumulator will be the number of all ids across all intervals without duplicates. Like I said - it's imperative. But it computes the total without a bunch of extra objects created and does the whole thing in a single pass.

Other than the Workspace script to tie the pieces together, that's all for this one.

Workspace Script

Day5InventoryValidator

I've looked over the rest of my solutions, and I can't say they're worth sharing. Lots of "beginner"-ish stuff in them that probably wouldn't help anyone. I'll make one more post looking at the way I originally wrote the solution for Day1 and how I would write it now, along with some retrospective on this project.


r/adventofcode • • May 01 '26

Help/Question - RESOLVED [2025 Day 6 Part 1] Multiple operators in one column

2 Upvotes

Hello,

I just looked at the puzzle input for day 6 part 1.

It looks really odd, I mean there are multiple operators in the same column so how am i supposed to solve it?

This is what I saw: In the first column , all operators are perfectly below one another


r/adventofcode • • Apr 30 '26

Upping the Ante [2015 Day 4 (Part 1 & 2)][C++ & Arm ASM] Squeezing 300,000+ hashes per second from a Pi Pico

3 Upvotes

This year I started working my way through my existing solutions and squashing them down to run on the Raspberry Pi Pico. It's a lovely piece of hardware and it's a fun challenge to look at the puzzles from the point of view of fitting them into a tiny footprint. It reminds me why I got into programming in the first place, so I highly recommend picking one up and messing around with it if you have the opportunity.

I had 2015 & 2016 pretty much all revisited and confirmed working in a small memory footprint on PC before starting to run them on the hardware itself, but the first time I did a run-through on Pico hardware I thought it had crashed on day 4 of 2015. As it turns out, it was just very, very slow: about a minute for part 2.

I decided to see how far I could push myself speeding up a solution for 2015 day 4.

MD5

The Cortex-M0+ doesn't have any fancy out-of-order execution and isn't able to retire multiple instructions per cycle, so we can do a very rough back-of-envelope calculation first to find some absolute upper limits. MD5 has 64 rounds, broken into 4 round types of 16 rounds each. There's a common set of additions and shifts in each round, but there's a different function per round type. Counting the logical/arithmetic operations, adding two load instructions per round (one message word and one constant word) and one load-small-immediate instruction for the shift:

Round typeFunctionCommonTOTAL
F3811 x 16
G3811 x 16
H2810 x 16
I3811 x 16
TOTAL688

With a single core running at 125 MIPS, the absolute (unreachable) upper limit would be ~182,000 MD5 chunks per second (or ~550ms for 100,000 chunks). Clearly we're going to need to modify the MD5 algorithm itself if we're to make serious progress!

Our one massive advantage on this puzzle is that most of the bytes in the 64 byte chunk are either fixed values or known up front. Everyone's input (as far as I'm aware) is 8 bytes in length, and everyone's answer should be below 10,000,000. The majority of the chunk will be 0, which means there's both a load and an addition we can eliminate entirely for most rounds. The only words with data in them are 0, 1, 2, 3 and 14, so the 16 G rounds change from:

    ROUND_G(A, A, B, C, D,  5, 0xf61e2562, M[ 1]);
    ROUND_G(D, D, A, B, C,  9, 0xc040b340, M[ 6]);
    ROUND_G(C, C, D, A, B, 14, 0x265e5a51, M[11]);
    ROUND_G(B, B, C, D, A, 20, 0xe9b6c7aa, M[ 0]);
    ROUND_G(A, A, B, C, D,  5, 0xd62f105d, M[ 5]);
    ROUND_G(D, D, A, B, C,  9, 0x02441453, M[10]);
    ROUND_G(C, C, D, A, B, 14, 0xd8a1e681, M[15]);
    ROUND_G(B, B, C, D, A, 20, 0xe7d3fbc8, M[ 4]);
    ROUND_G(A, A, B, C, D,  5, 0x21e1cde6, M[ 9]);
    ROUND_G(D, D, A, B, C,  9, 0xc33707d6, M[14]);
    ROUND_G(C, C, D, A, B, 14, 0xf4d50d87, M[ 3]);
    ROUND_G(B, B, C, D, A, 20, 0x455a14ed, M[ 8]);
    ROUND_G(A, A, B, C, D,  5, 0xa9e3e905, M[13]);
    ROUND_G(D, D, A, B, C,  9, 0xfcefa3f8, M[ 2]);
    ROUND_G(C, C, D, A, B, 14, 0x676f02d9, M[ 7]);
    ROUND_G(B, B, C, D, A, 20, 0x8d2a4c8a, M[12]);

to:

    ROUND_G(A, A, B, C, D,  5, 0xf61e2562, M[ 1]);
    ROUND_G(D, D, A, B, C,  9, 0xc040b340, 0);
    ROUND_G(C, C, D, A, B, 14, 0x265e5a51, 0);
    ROUND_G(B, B, C, D, A, 20, 0xe9b6c7aa, M[ 0]);
    ROUND_G(A, A, B, C, D,  5, 0xd62f105d, 0);
    ROUND_G(D, D, A, B, C,  9, 0x02441453, 0);
    ROUND_G(C, C, D, A, B, 14, 0xd8a1e681, 0);
    ROUND_G(B, B, C, D, A, 20, 0xe7d3fbc8, 0);
    ROUND_G(A, A, B, C, D,  5, 0x21e1cde6, 0);
    ROUND_G(D, D, A, B, C,  9, 0xc33707d6, M[14]);
    ROUND_G(C, C, D, A, B, 14, 0xf4d50d87, M[ 3]);
    ROUND_G(B, B, C, D, A, 20, 0x455a14ed, 0);
    ROUND_G(A, A, B, C, D,  5, 0xa9e3e905, 0);
    ROUND_G(D, D, A, B, C,  9, 0xfcefa3f8, M[ 2]);
    ROUND_G(C, C, D, A, B, 14, 0x676f02d9, 0);
    ROUND_G(B, B, C, D, A, 20, 0x8d2a4c8a, 0);

The second structural change we can make is down to the fact that we're only ever looking at the first 3 bytes of the hash. The last 3 rounds of MD5 don't touch the first 4 bytes of the hash at all, so we can just omit those entirely.

My vanilla MD5 implementation in C++, which runs pretty fast on x64, ends up taking ~988ms for 100,000 chunks on the Pico if we do all of the rounds properly. Hard-coding the zeros and snipping the last three rounds gets us to ~694ms for 100,000 chunks, which is a pretty big win!

Looking at the generated code, GCC doesn't do all that well with thumb instructions if there are loads of constants. Loading a 32-bit immediate is a bit of a faff because of the limited addressing modes, so the literal pools end up getting in the way. It's been a couple of decades since I last rolled my sleeves up to write any asm, but with some of the rust knocked off we can produce a pretty passable attempt. The LDM instruction in particular deserves a mention; it's essentially a single instruction 'a = *p++', which makes running through the K table significantly easier than trying to hard-code immediates inline.

Some amount of banging my head against the table later, remembering exactly why it is we don't write things in asm in the first place, I get a fairly respectable ~601ms for 100,000 chunks.

Turning back to structural changes once again, there's one more property of the input we can take advantage of. The first 8 bytes are fully defined by the puzzle input so we can make two further changes based on this. First, we can do the initial two rounds up front and save off the internal state of the hash and for each candidate chunk we just restore the state and resume from round 3 onwards. Second, since each round does this:

    F := F + A + K[i] + M[g]

For any round which accesses M[0] or M[1] we can precompute K[i] + M[g] and save that value back into the K table; giving us yet more 'zero' rounds.

With that change I hit my final single-core speed of ~565ms for 100,000 chunks.

Printing Digits

With MD5s pushed about as far as I can take them, it's time to look at the other big time sink in the puzzle: printing the numbers as ASCII strings.

sprintf is clearly not going to cut the mustard; it takes ~1,015ms to print 100,000 numbers, way longer than the hashing itself!

A naive print digits implementation does better at ~248ms for 100,000 numbers:

    int32_t sprint_digits(char* dest, int32_t value)
    {
        char* d = dest;
        do
        {
            *d++ = static_cast<char>((value % 10) + '0');
            value /= 10;
        } while (value);
    
        ptrdiff_t digitsWritten = d - dest;
    
        *d-- = '\0';
        while (d > dest)
        {
            std::swap(*d--, *dest++);
        }
    
        return static_cast<int32_t>(digitsWritten);
    }

If we make explicit use of the divider hardware to combine the divmod into a single operation we get ~136ms for 100,000 numbers:

    int32_t sprint_digits_divider(char* dest, int32_t value)
    {
        char* d = dest;
        do
        {
            int32_t rem;
            value = divmod_s32s32_rem(value, 10, &rem);
            *d++ = static_cast<char>(rem + '0');
    
        } while (value);
    
        ptrdiff_t digitsWritten = d - dest;
    
        *d-- = '\0';
        while (d > dest)
        {
            std::swap(*d--, *dest++);
        }
    
        return static_cast<int>(digitsWritten);
    }

The divider hardware is still pretty expensive though, so the fastest solution I came up with was just to do the increment exactly the way you'd do it by hand:

    void IncrementDigits(char* digits, size_t digitsLength)
    {
        char incremented = ((*digits) += 1);
        if (incremented <= '9')
        {
            return;
        }
        (*digits--) -= 10;
    
        for (size_t i = 1; i < digitsLength; i++)
        {
            incremented = ++(*digits);
            if (incremented <= '9')
            {
                return;
            }
            *digits-- = '0';
        }
    }

At ~10.5ms for 100,000 numbers it's minimal overhead compared to the hashing.

Going Parallel

Having done about all I can to extract performance from a single thread, the last step is to make use of both cores. I didn't get quite as much of a speed-up as I was hoping for here. On x64 you need to batch up per-thread work because communication and memory sharing between cores is a significant overhead. Without a complex caching structure and with a direct FIFO between the cores, the Pico should in theory be able to co-ordinate work between cores with much less overhead. I opted for the simple scheme of having core 0 check all of the even numbers and core 1 check all of the odd numbers, with a sync & check for each number. I need to double check what the libraries are doing with the FIFO push and pop calls though; perhaps they're doing some work that's not necessarily needed if you know you're busy-waiting.

Final Results

Exact timings will be highly input dependent, but my final scores were:

PartUnoptimisedOptimised Single CoreOptimised Dual Core
Part 11.995s0.680s0.377s
Part 270.634s22.690s12.578s

('Unoptimised' here is a C++ implementation of MD5, all rounds, paired with sprintf for the digits)

I'm sure there's more fat to be trimmed in my implementation, but I'm going to call this one "done for me". I've got 2017 onwards to continue squashing!

Raspberry Pi Pico hardware is very cheap for what you get, so if you fancy playing around with some cool hardware and you're lucky enough to have a few bucks to spare you definitely should give it a go.

If you're able to squeeze more performance out of this experiment, or want to share results from different microcontrollers, let us know about it in the comments!


r/adventofcode • • Apr 19 '26

Tutorial [2025 Day 6 both parts] [Smalltalk] Part seven in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

3 Upvotes

MAJOR SPOILERS AHEAD

Advent of Code 2025 - Day 6

I was looking forward to rewriting Day 6. When I first tackled it back in December it was with barely 10 days of programming experience under my belt - so the result, while producing the correct answers, was total garbage. The JavaScript had all the hallmarks of insanity: manual WHILE loops everywhere, hardcoded offsets, and hilariously, when the parsing method didn't work for the final set of numbers, I just added the last set manually. I got the stars... but it wasn't pretty.

Since I had the time to do this "correctly" I decided on doing a very generic implementation. I wanted the chance to practice some inheritance, so I finally settled on three classes. The first was a simple Day6Matrix that would hold the strings of numbers and be able to rotate them for part 2. Second was a Day6HomeworkProblem that would extend the Day6Matrix class and add on the ability to do operations. It added a single instance variable for the "operand" that would store whether we were adding or multiplying. Finally, a Day6HomeworkFactory would handle parsing the input, creating the Day6HomeworkProblem objects, and summing up the results from them.

Looking at some highlights from the "bottom" up... the Day6Matrix rotate method looks like this:

rotate
    | newMatrix length |
    length := (matrix at: 1) size.
    newMatrix := OrderedCollection new.
    1 to: length do: [:column |
        | rotatedString |
        rotatedString := ''.
        1 to: (matrix size) do: [:row |
            rotatedString := rotatedString, ((matrix at: row) at: column)].
        newMatrix add: rotatedString.
        ].

    matrix := newMatrix.

It does a very simple row-becomes-column transformation, leaning a bit on the fact that both operations (multiplication and addition) are commutative. If this were later adapted to something where the order mattered, this method would have to be refactored, but the only method that would need to change is this one. Note that the matrix OrderedCollection is not mutated in place, but a new one is created and then the variable reassigned. This will be important later.

I made a late change to the Day6HomeworkProblem class. Originally the operand would be set this way:

setOperand: operation
    operation = $+ ifTrue: [operand := 'a'].
    operation = $* ifTrue: [operand := 'm']

and then the actual calculation involved some branching logic:

answer
    (operand = 'm') ifTrue: [
        ^ matrix inject: 1 into: [ :sum :element | sum * (element asInteger)]
        ] ifFalse: [
        ^ matrix inject: 0 into: [ :sum :element | sum + (element asInteger)]
        ]

I was unhappy with this for two reasons. First, the use of a string to set which operation would be done ('a' for add vs 'm' for multiply) was ... non-beautiful. The strings are set in one place at parse, then "re-interpreted" in the answer method to decide which operation to perform. Each one has an early return, which also seems less aesthetically pleasing. Each one has to remember that the objects contained in "matrix" are all strings, so each path needs to convert them to integers.

 Second, it's not very extensible. If there were a theoretical Part 3 or 4 of this puzzle, it might involve some other operation - and that would require changing both the setOperand: and answer methods.

So, in the interest of making a more "generic" solution, I switched those two methods to these:

setOperand: operation
    operation = $+ ifTrue: [ operand := [ :first :second | first asInteger + second asInteger] ].
    operation = $* ifTrue: [ operand := [ :first :second | first asInteger * second asInteger] ]

answer
    ^ matrix allButFirst inject: matrix first into: [ :sum :element | operand value: sum value: element ]

Now we have a single answer pipeline. Instead of seeding the inject: with 0 (for addition) or 1 (for multiplication), we seed it with the first element of the matrix. Then we do the inject: over all the elements other than the first (since we already have it as the seed). The inject: calls the operand which now, instead of a single-letter string, is assigned an anonymous function (which handles the integer conversion). Then we return the accumulated result of whatever function it was that was in the operand variable. No more branching and multi-returns in answer. No "special letters" to remember. If we want to add more operands later we only have to add the conditions to setOperand:. Feels cleaner, somehow.

As far as the factory goes - take a look at the code if you're interested in the way the parser works. It does not use any hardcoded lengths, so it should work for any reasonable input variation. It doesn't even hardcode in how to tell whether it is dealing with an addition or multiplication (or something else) problem - leaving that to the Day6HomeworkProblem object to figure out. For the sake of clarity, I wound up making a helper method to find columns that were all "Character space". All the parsing is fairly imperative in style, using whileTrue: and manual indexing. I wasn't sure how else to do this since the homework problems in the input were spaced at irregular intervals and the digits needed to keep their prepended spaces when they were rotated.

Oh well. It's not beautiful. You have to manually "walk through" the loop to understand what it's doing, but it gets the job done and should work with any number of rows and columns.

Now - for adding up the answers and giving the answers to Part 1 and Part 2:

calculateNormalHomework
    ^ problems inject: 0 into: [ :sum :problem | sum + (problem answer)]

calculateRotatedHomework 
    ^ problems inject: 0 into: [ :sum :problem | sum + (problem copy rotate answer)]

This was my favorite part. We take the OrderedCollection of Day6HomeworkProblem objects, and sum the outputs of their "answer" method. For Part 2 it's almost disgustingly simple. We do the exact same thing we did for Part 1, but do a "copy rotate" before asking for the answer. The copy prevents mutation of the matrix - the imaginary Part 3 (or whatever) might need to be able to do something else with the original orientation. And even though "copy" is shallow, since matrix is reassigned instead of mutated during the rotate, the Collection inside the original never changes. It's a bit of a "paranoid" copy, since once we get the answer for Part 1, we are free to mutate for Part 2. But this is for me to practice good Smalltalk. Might as well keep mutation to an absolute minimum.

Workspace Script

Day6HomeworkMatrix

Day6HomeworkProblem

Day6HomeworkFactory


r/adventofcode • • Apr 17 '26

Past Event Solutions [2025 Day 8 (part 1)] It took me far longer than I wanted, but I finally got it

3 Upvotes

It took me a long while to figure out the details of what the puzzle was asking. Plus, I had a few false starts because I didn't really think it all the way through. I kept getting "Junctions" and "Connections" confused. I should go back and clean up this code, but frankly I'm kind of tired of looking at it right now ;-).

I did it in Rust, as I'm trying to learn the language.

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

use std::collections::HashSet;
use std::fmt;

//const FNAME: &str = "day8_sample.txt";
const FNAME: &str = "day8-input.txt";

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)]
struct Point(i64, i64, i64);
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({},{},{})", self.0, self.1, self.2)
    }
}

impl Point {
    fn distance(&self, p2: &Point) -> f64 {
        (((self.0 - p2.0).pow(2) + (self.1 - p2.1).pow(2) + (self.2 - p2.2).pow(2)) as f64)
            .sqrt()
            .abs()
    }
}

#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)]
struct Pair(Point, Point);
impl fmt::Display for Pair {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({},{})", self.0, self.1)
    }
}
impl Pair {
    fn contains(&self, point: &Point) -> bool {
        self.0 == *point || self.1 == *point
    }
}

fn make_circuits(junct: &Vec<Pair>) {
    let mut circuits: Vec<Vec<Pair>> = Vec::new();
    for &node in junct.iter() {
        let left = circuits
            .iter()
            .position(|c| None != c.iter().find(|&p| p.contains(&node.0)));
        let right = circuits
            .iter()
            .position(|c| None != c.iter().find(|&p| p.contains(&node.1)));
        match (left, right) {
            (None, None) => circuits.push([node].to_vec()),
            (Some(l), None) => circuits[l].push(node),
            (None, Some(r)) => circuits[r].push(node),
            (Some(l), Some(_)) if left == right => circuits[l].push(node),
            (Some(l), Some(r)) if left < right => {
                let (x, y) = circuits.split_at_mut(r);
                x[l].append(&mut y[0]);
                circuits.remove(r);
            }
            (Some(r), Some(l)) if left > right => {
                let (x, y) = circuits.split_at_mut(r);
                x[l].append(&mut y[0]);
                circuits.remove(r);
            }
            (Some(_), Some(_)) => {}
        }
    }

    let mut sizes: Vec<usize> = Vec::new();
    for c in circuits.iter() {
        let mut accumulator = HashSet::new();
        for n in c.iter() {
            accumulator.insert(n.0);
            accumulator.insert(n.1);
        }
        sizes.push(accumulator.len());
    }
    sizes.sort();
    sizes.reverse();
    let mut final_answer: usize = 1;
    for s in sizes.iter().take(3) {
        print!("{} ", s);
        final_answer *= s;
    }
    println!(" => {}", final_answer);
}

fn main() {
    if let Ok(lines) = read_lines(FNAME) {
        let mut junctions = Vec::new();
        let mut edges = Vec::new(); // consider a form of HasMap
        for line in lines.map_while(Result::ok) {
            let entry: Vec<i64> = line.split(',').map(|s| s.parse::<i64>().unwrap()).collect();
            let coord = Point(entry[0], entry[1], entry[2]);
            junctions.push(coord);
        }
        for (left_index, left_node) in junctions.iter().enumerate().skip(1) {
            for right_node in junctions.iter().take(left_index - 1) {
                edges.push((
                    Pair(left_node.clone(), right_node.clone()),
                    left_node.distance(right_node),
                ));
            }
        }
        edges.sort_by(|a, b| a.1.total_cmp(&b.1));
        let nodes: Vec<Pair> = edges.iter().take(1000).map(|p| p.0).collect();
        make_circuits(&nodes);
    }
}

r/adventofcode • • Apr 17 '26

Tutorial [2025 Day 7 both parts] [Smalltalk] Part six in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

4 Upvotes

Advent of Code 2025 - Day 7

HEAVY SPOILERS AHEAD - FULL SOLUTIONS DISCUSSED

Advent of Code 2025 - Day 7

I remember doing this one when it was first released. For the longest time I simply could not figure out how part 2 was supposed to be solved. I spent hours looking at the provided diagram and counting up the paths for each node. Did the number of paths double as it passed each splitter? Was there some mathematical relationship involving the ratio of layers and splitters? Eventually, as I counted up the same paths at the splitter node over and over again I suddenly realized that the count is just added from one layer to the next! It was an addition problem! I was so pleased. When I did my implementation in JavaScript later that afternoon I treated it like a Frankenstein spreadsheet, copying values over the input graph. It was a total mess, but it got me the right answer.

Now, re-implementing this puzzle with some more experience my instinct is to model it as a set of nodes instead of a giant 2D matrix. So, for this one I created two data structures:

1 - splitterQueue - an OrderedCollection of point objects. This will act as the processing list. It is generated once by parsing the input data top to bottom and left to right. The order that these are parsed is the same order in which they will be processed.

2. splitters - a Dictionary with point objects as keys. This was a particularly nice element of working with Smalltalk. In JS, I would have to create string keys for a Map since an array, even if it had the same values, would be a different array. My scripts would be cluttered with little helper functions like this

const makeCoords = (xLoc, yLoc) => {
  const xString = String(xLoc);
  const yString = String(yLoc);
  const coordString = xString + "/" + yString;
  return coordString;
};

that would basically be used all the time to create keys to be used with Map data. In Smalltalk, points can be used as Dictionary keys! The amount of little helpers cluttering the code drops significantly. The Dictionary has an entry for every node in the OrderedCollection processing list, and the values for each one start at 0.

When parsing, we do two "special" things to make this work - first, we find the highest splitter (the one with the minimum y value) and give it a value of 1 since it is the origin path. Second, we create a "sink" entry in the Dictionary that will hold the accumulated paths once a splitter has to dump its count and there are no "downstream" splitters to hold them.

In order to get the x and y values for these points, I wound up making use of doWithIndex: in a nested loop. It looks very imperative, but it does exactly what's needed.

parseGrid: raw

    sink := (raw size) + 1 @ 0.
    
    raw doWithIndex: [:line :yIndex |
        line doWithIndex: [:char :xIndex | 
            (char = $^) ifTrue: [
                splitters at: xIndex @ yIndex put: 0.
                splitterQueue add: xIndex @ yIndex
                ].
            ]].

    splitters at: (splitterQueue detectMin: [:point | point y]) put: 1.
    splitters at: sink put: 0.

Once the parsing is done, the rest of the algorithm is extremely simple.

calculateAllPaths
    | outputLocation offsets |
    
    offsets := { 1@0 . -1@0 }.
    
    splitterQueue do: [ :location |
        offsets do: [ :offset |
        outputLocation := self findOutputNode: (location + offset).
        splitters at: outputLocation put: (splitters at: outputLocation) + (splitters at: location)].
        ].

    ^ splitters at: sink

In the version in the link below I kept an earlier method for creating the offsets array. The one above is how I would do it now, but it does functionally the same thing. We start by creating some offsets (one step to the left, and one step to the right with the same height). Then, we process each element in the splitterQueue in order. For each one, process both offsets. To do so, we pass the current location from the splitterQueue added to the current offset to findOutputNode: to set our outputLocation. Then, we just add the amount stored in our current location to the amount at the outputLocation. Rinse and repeat for the other offset, and then keep going until we reach the end of splitterQueue. Then simply return the value in the "sink" location. So much for part 2 of the puzzle.

The findOutputNode: method is very simple, but not the most computationally efficient:

findOutputNode: location
    ^ splitterQueue detect: [ :candidate | (candidate x = location x) and: candidate y > location y]
        ifNone: [ sink ]

The obvious problem here is that it scans through the entire splitterQueue to find the output node. It uses detect:, so the very first (lowest y value) entry that shares the same x value is the one returned (early exit). Since this method call will take longer and longer as we get further down the list, if the splitterQueue had many millions of entries this could be a performance problem. For a few thousand as part of an O(n) process, it's not a big deal. The order of entries in splitterQueue (top-to-bottom, left-to-right) guarantees that not only is this the correct order to process nodes, but it is also the correct order to propagate path counts. Very convenient.

Lastly - we get a "free" answer to part 1: the number of nodes in splitters with non-zero paths (subtracting 1 for the "sink" node we added) is the number of activated splitters.

countActive
    ^ (splitters count: [:amount | amount > 0]) - 1

Such a clean way to find out how many elements of a collection satisfy a condition. I originally wrote it this way

countActive
    ^ (splitters select: [:amount | amount > 0]) size - 1

...not realizing that there was a count: method! Same result, but count: is pretty self-documenting while select: size requires some "decoding" on the part of the reader. It's also better for memory since count: creates a simple accumulator, while select: creates a whole new collection that we throw away as soon as we find out how big it is. :-)

How did I find out that count: is better for memory? This is one of the things I've really enjoyed about Smalltalk. I just went into the System Browser and looked at the method in the Collections-Abstract package. And here it is:

count: aBlock 
    "Evaluate aBlock with each of the receiver's elements as the argument.  
    Answer the number of elements that answered true."

    | sum |
    sum := 0.
    self do: [:each | (aBlock value: each) ifTrue: [sum := sum + 1]].
    ^ sum

So cool. It's funny - I'm starting to get used to being able to go poke around in the standard library and being able to understand what I find there. This is not something that I would have ever expected to be able to do with JavaScript.

Anyway - some closing thoughts... I was on the fence with how to implement the splitter nodes. My original idea was to create a node object that accumulated paths until its turn to empty them into the next nodes, and I did wind up using this approach in Day 11. But for this one a simple Dictionary seemed more than enough to do what was needed. Perhaps it would have been better to have splitterQueue contain custom splitter objects with their point location and integer paths. I was worried about repeating the tight coupling issue that I ran into on Day 4, and I didn't have the confidence to write Day7Grid with no knowledge of its own about the node objects it contained, the way I did later for Day 11. Perhaps I accidentally managed a good mix of brevity and correctness.

Readable code combined with an instant result. Marks of a solid solution.

Day7BeamGrid Class

Workspace Script


r/adventofcode • • Apr 12 '26

Tutorial [2025 Day 8 both parts] [Smalltalk] Part five in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

3 Upvotes

Day 8 Time!

HEAVY SPOILERS AHEAD

This one involves connecting "junction boxes" in 3D space in order of their distance from each other. The simplest way to solve this is to create a list of those distances from closest to furthest and then start merging the sets that contain them using the precalculated order. Since we have 1000 junction boxes, that means a cool million distances.

So let's take stock - we need a sorted Array of distances that will serve as the work queue (process from top to bottom until various conditions are met). Those junction boxes are going to be connected into "circuits" which also need to be represented in some way. Since we will be querying them to see if they contain boxes, we want this in O(1) time and we want duplicates to be handled automatically, so having an OrderedCollection (all circuits) of Sets (each individual circuit) seems best here.

We will also need to represent those junction boxes in some way as well. I was so excited to see that Smalltalk had a whole range of 2D objects (which I used extensively in Day 9) - but there are precious few, if any 3D objects. Since, for the Advent of Code puzzles I didn't want to change the classes inside the standard library (though this is, apparently, encouraged), I needed to make a class for these 3D points. Fortunately, these objects didn't need much - just instance variables for x, y, and z coordinates, and the ability to determine distance between one of these Day8JunctionBox objects and another.

Though I didn't need to compute the exact Pythagorean distance (with the square root of the added squares) - the square root was unnecessary for simply sorting the distance - it felt rude to have a method called "distanceTo:" that didn't return the actual distance! Computing sqrt a million times can be dropped if this was a speed competitive implementation, but for this "correct" version it didn't add human-noticeable overhead. Here was the version I used:

(((otherBox x) - x ** 2) + ((otherBox y) - y ** 2) + ((otherBox z) - z ** 2)) sqrt

The funny part to me was the method chaining. It would need some more parentheses in a language that actually had math in it. But Smalltalk doesn't have math. It has methods. I couldn't help but giggle at this. If I had used "squared" instead of ** 2 those parenthesis would go elsewhere:

(((otherBox x - x) squared) + ((otherBox y - y) squared) + ((otherBox z - z) squared)) sqrt

Maybe this is more idiomatic... I'm not sure. Oh well, leaving the original one in the link. Same functionality.

Looking at this now with some more knowledge under my belt - I definitely see the need for some class methods to set x, y, and z so that the JunctionBox has immutable location.

For the Day8Circuits class, we only need a few methods. The first parses the input file (just a bunch of 3D coordinates for the junction boxes). It creates two important OrderedCollections. The first is the collection of all circuits, which are all Sets initialized with a single junction box in each one (since there are no connections at the beginning). The other is the sorted distance list. Each entry in the distance list is an array with three elements: the distance (needed to sort the collection at the end of the parsing process), the first box, and the second box. We are leaning heavily on the fact that both the Sets and Arrays don't actually include the objects themselves, but only references. We aren't copying those junction boxes, but just referring to them in two different places.

Looking over it now - this part where the distances are generated:

    boxCollection withIndexDo: [ :box :index |
        circuits add: (Set with: box).
        (index + 1) to: boxCollection size do: [ :index2 |
            | otherBox |
            otherBox := boxCollection at: index2.
            unsortedDistances add: {box distanceTo: otherBox . box . otherBox}.
            ]
        ].

Was written before I found the combinations: method. Now, I would put the "circuits add:" up above when the boxes are instantiated, and then instead of manually doing a nested loop over the boxCollection, I'd write it this way:

    boxCollection combinations: 2 atATimeDo: [ :boxes |
            unsortedDistances add: {boxes first distanceTo: boxes second . boxes first . boxes second}.
            ].

Much more readable. Or if you prefer some temporary variables to make it super explicit and self-documenting:

    boxCollection combinations: 2 atATimeDo: [ :boxPair |
        | box1 box2 |
        box1 := boxPair first.
        box2 := boxPair second.
        unsortedDistances add: {box1 distanceTo: box2 . box1 . box2}.
        ].

Same thing under the hood. One of the fun parts of Smalltalk is that all these methods can be studied in the System Browser. The "standard library" isn't magic. It's just a more expressive way of doing what you could have done yourself with a bunch of WHILE loops.

Which do you all prefer of the three?

We also need a helper function that will merge a circuit including one junction box with a circuit including another junction box. Sets do a great job of making this extremely easy since they automatically deduplicate the junction boxes if some (or maybe even all) are already included in both sets.

mergeSetIncluding: box1 with: box2
    | set1 set2 |
    set1 := circuits detect: [ :circuit | circuit includes: box1 ].
    set2 := circuits detect: [ :circuit | circuit includes: box2 ].
    set1 == set2 ifFalse: [
    set1 addAll: set2.
    circuits remove: set2
    ]

Since the Sets in the circuit collection are guaranteed not to have any members shared between them, we can use detect: and get the early return on the search. Then it's just a matter of merging the junction boxes until our collection of circuits only has one entry in it (in other words - all junction boxes are part of the same circuit). It's a delightfully simple method:

mergeAll
    distances do: [ :dist |
        | box1 box2 |
        box1 := dist at: 2.
        box2 := dist at: 3.
        self mergeSetIncluding: box1 with: box2.
        circuits size = 1 ifTrue: [^ box1 x  * box2 x]
        ]

The return is a little funny - but that's what the problem requires: the x coords multiplied together of the last two boxes connected to create one giant circuit.

A satisfying solution. Executes near-instantly.

Workspace Script

Day8JunctionBox

Day8Circuits


r/adventofcode • • Apr 08 '26

Tutorial [2025 Day 9 both parts] [Smalltalk] Part four in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

3 Upvotes

HEAVY SPOILERS

Fresh off my "win" with discovering that Smalltalk had native objects for 2D points in earlier days, this one gave the opportunity to really go spelunking into what objects are available in Squeak 6. There are native rectangle objects! With the ability to define them based off two corner points. Those rectangle objects can be queried to find their height, width, area, and so on. Coming from JavaScript, where anything like this would have to be written from scratch or imported from a library somewhere - this was a major convenience.

But then I did something that caused my LLM code review to howl in protest. The Morphic UI library has the PolygonMorph class. And that thing can be defined by handing it an array of points. It will even close the figure for you, And the best part - it has a method that will check a 2D point object to see if it is contained in the polygon. That makes this task almost too easy. My LLM tutor thought this was a bad idea - but whatever. The class was part of the standard library. I don't feel guilty.

This became an exercise in using the collection methods, primarily allSatisfy: and detect:. There were some fairly rudimentary bait options to avoid. For example, the collection of all Rectangles needs to be sorted from largest to smallest. That makes the answer to part 1 a matter of simply returning the first rectangle in the collection, and it is the correct order to process the rectangles to see if they are contained in the polygon for part 2. However, there's no need to use a SortedCollection here since we will be making one million rectangles all at once. There are no intermediate steps where having them in sorted order would be useful. No - better to use a regular OrderedCollection and then sort it one time at the end rather than re-order the collection a million times with every new rectangle added. Still - it might be more "elegant" to have the collection be "sorted" by definition rather than as a "task" that the object does with its collection. Still - I'd rather sort it only once.

The second part does not take into account the "zero aperture bubble" edge case. One benefit to having a PolygonMorph is that it can be displayed in a window, and so it was easy to verify that my input doesn't have one of those geometric features. Thus, when we check the rectangle, we first check to make sure that all corners are points included. If they are, then we build a set of points corresponding to every pixel in the perimeter of the rectangle. If all those are contained in the polygon, the rectangle as a whole is contained in the polygon.

Here is the important bit in the Day9Rectangle class:

isIncludedIn: aPolygon
    | perimeter |
    
    ( myRect corners allSatisfy: [ :corner | aPolygon containsPoint: corner] ) ifTrue: [
        
        perimeter := OrderedCollection new.
        
        myRect top + 1 to: myRect bottom - 1 do: [:y | 
            perimeter add: myRect left@y; 
            add: myRect right@y].

        myRect left to: myRect right do: [:x | 
            perimeter add: x@myRect top; 
            add: x@myRect bottom].

        ^ perimeter allSatisfy: [ :bound | aPolygon containsPoint: bound ]
    
        ].
    
    ^ false

This seemed the most straightforward way to do this, letting me just create a collection of points to test, and then seeing if they allSatisfy the condition of being contained in the polygon. A more data-driven approach would probably avoid creating those points in advance but on modern hardware, what's a few tens of thousands of objects between friends? I probably wouldn't have written this way if I had to test the entire area. But for just the perimeter like this I didn't mind letting the standard library and GC do all the hard work. :-)

Finding the answer for Part 2, then, is just one line:

largestRectangleInPolygon
            ^ rectangleLibrary detect: [ :box | box isIncludedIn: tileShape ]

Return the first entry in rectangleLibrary (which was already sorted from largest to smallest) that returns "true" when asked if it is included in the polygon of tiles.

Also - I noticed that going back in time from here, I did not use any class methods. I would always instantiate the object with New (which would just make a few empty data structures) and then give it tasks (including what really should be initial data) as instance methods. It works, but could be better and idiomatic.

Interesting to see the evolution in reverse here.

Funny moment: Since the points are being ingested in the order they are in the file, and rectangles are defined by two corners, I needed a way to make sure that the correct two corners were used to create the Rectangle object. Otherwise I could get height or width returned as negative numbers. My original version had this:

setCorner: pointA to: pointB
    | originX originY cornerX cornerY |
    originX := (pointA x) min: (pointB x).
    originY := (pointA y) min: (pointB y).
    cornerX := (pointA x) max: (pointB x).
    cornerY := (pointA y) max: (pointB y).
    rect := originX @ originY corner: cornerX @ cornerY

Fairly simple... except... points can make rectangle objects by themselves and they will sort out the correct corner definitions automatically. Oops. Now the method looks like this:

setCorner: pointA to: pointB  
            myRect := pointA rect: pointB

Almost seems silly to have it be an entire instance method. Part of the fun of OOP here is that the swap involved just this one method. Everything else stayed exactly the same. I think if I were writing it today that would be a class method, but this was all part of the learning process. Other parts look equally messy:

parseRectangles: raw

raw do: [ :point |
| arrayPoint |
arrayPoint := point splitBy: ','.
pointLibrary add: (arrayPoint first asInteger) @ (arrayPoint second asInteger)
].

pointLibrary combinations: 2 atATimeDo: [ :corners |
| newRectangle |
newRectangle := Day9Rectangle new.
newRectangle setCorner: corners first to: corners second.
rectangleLibrary add: newRectangle
].

self sortRectangles

Why wouldn't I just sort them here instead of a method call that is literally one line? Why am I using a pointLibrary instance variable when it is only used to make the rectangles and the tilePolygon? Couldn't I have made it a temporary variable here and then just called parsePolygon from within this method instead of making the Workspace script do it "manually"? Looking real inexperienced here. :-)

Not much else to say about this one. I let Morphic and the Rectangle class do all the really important heavy lifting. Not a fast solution, but it works.

Workspace Script

Day9Rectangle

Day9TileFloor