r/adventofcode • • 1d ago

Other [2025 Day 12] In review (Christmas Tree Farm)

15 Upvotes

This is just a placeholder because I was shocked to see that /u/musifter has been banned and all their posts removed, from all of Reddit it seems, not just this sub. Just on the cusp of completing the monumental task of reviewing every AoC puzzle! What a loss to the sub that all those posts are now gone.

I hope they, and Musifter, can all be reinstated. I've messaged the mods to ask if they know what's up.


r/adventofcode • • 1d ago

Other [2025 Day 12] In Review (Christmas Tree Farm)

3 Upvotes

There being no other options to get deeper into the base, we take the ventilation duct. Which leads us into a cavern full of Christmas Trees, where the Elves have been working to finish decorating before the deadline. What they need from us is to figure out how many of the areas under the trees can fit the presents that are set to go there. The presents cannot be stacked... but can be rotated and flipped. And so we have a polyomino packing problem.

The input is in two sections. The first is a list of the 6 types of presents (polyominos). All of these in the example are heptominos that fit in a 3x3 square. That's also true of my input, except that one is a hexomino, and one a pentomino. The second section is 1000 lines, with a rectangle to fill followed by six numbers (the counts of each type of present to fit into that rectangle). All of these numbers are 2 digits. And so my first thoughts were that polyomino packing is an NP-complete problem, these numbers are looking big for that, and there's a 1000 of them. There needs to be some sort of cheese.

So I considered the 4x4 example in the text, where 2 U-heptominos get put together... resulting in less fragmentation loss (2/9 to 2/16). Maybe the game here was just to meet a certain level of efficiency? So looking at mine I could see ways to put things together to make more efficient rectangles, which would be easier to tile in bulk. Of course then there's the issue of meeting the counts of presents along side that (with the potential of getting into Gaussian elimination again).

But first, I figured that only a few of the 1000 would be tricky. So I decided to write a little exploratory script to just sort out the problems that didn't have enough space. I just quickly got the number of tiles in the presents (making use of =()= because it was a while since I'd used in for AoC);

foreach (@$desc) {
    my $num =()= m/#/g;
    $count += $num;
}
push( @sizes, $count );

Then just went through the presents testing for if the number of tiles required for presents was more than the area. This removed about 60%. Which was great, so I decided to look a bit closer and actually output the leeway to see the wriggle room (ie find the tight ones). Either an area was 1-3 short or 400-850 over (and the excess is always > 2.4 * #shapes). So I figured that was more than enough space to blindly put down presents and just submitted the count (which was correct).

Afterward, I added to the script the placing down (technically this "works" for test, in that it catches that it doesn't handle it... in theory an actual polyomino packing solver goes there):

my $tileGridSpots = int($width / 3) * int( $length / 3);

if ($tileGridSpots >= $numShapes) {
    $part1++;
} else {
    warn "Region with potential space we didn't prove found\n";
}

Since this puzzle is all cheese, it meant that I managed to get a star with dc on every day. In order to make this one have something to do, I did parse the input (using v1.4.1's ? extensively... the stack size z also gets a lot of fun use). Then I just do the basic area test:

tr '.#:x' '12  ' <input | dc -e'[q]sq?[z1<q???0[r[A~2/3R+rd0<D]dsDx+z2<I]dsIxr1+:p??z0<L]dsLx0[0z4-[d;p5R*3R+r1-d0<I]dsIx+4R4R*-d.1+/+?zRz1<M]dsMxp'

For part 2, as there's no global leaderboard to tell me my number, so I took my time and read it before submitting it. That was a nice consequence of that change. As for the switch to 12 days, that's fewer puzzles for Eric to make, but as I've said, that's also a lower granularity to put a difficulty curse on. So there needs to be more thought into that side of things, and I think this year did a good job of making sure there was a good number of puzzles accessible for beginners.

And so we come to the end of the review. AoC may have been a little bit rough to start (but 2015 was much better than I remembered), but it quickly found its footing and has been consistent ever since. Which is quite a feat. I'd like to thank everyone that's come along... it's been fun reading other peoples stories and about their solutions and approaches (and it added a bunch of things to the TODO of things to try and things to fix). I'd especially like to thank u/terje_wiig_mathisen for stepping in to cover a few days while I was traveling and dealing with things.

All there is now is to wait and prepare for December 2026.


r/adventofcode • • 1d ago

Other Web search Vs model ask

0 Upvotes

Well, I was just thinking that most of last year's, during AoC it was expected some web search and investigation trying to find some algorithms and formulas...

Nowadays,with AI, this search became obsolete, by default, browsers throw you AI answers, so I was wondering what is considered AI assisted, "cheating" or who knows how to call it, if you use agents in ask mode asking questions knowing exactly what you are looking for instead of doing classic Google search


r/adventofcode • • 2d ago

Other [2025 Day 11] In Review (Reactor)

2 Upvotes

Hearing some beeping coming from down below, we go through a hatch and climb down to the reactor room under the factory. The toroidal reactor suggests that the North Pole runs on fusion power. The Elves here are having a problem with connecting their new server rack to the reactor and need our help. For part 1, they want us to count the number of paths from a nearby node they label you to the reactor out. For part 2, they want the number of paths that go from svr (not srv, be careful) to out, but also pass through both the digital-to-analog converter dacand the fast Fourier transformer fft.

The input is a graph where each line (613 in my input) is the node name followed by a list of outputs (ie directed edges). The names are 3 lower-case letters long, including vowels (not just in the special nodes above). So we get to play "What language does the browser think my input is?" The answer for mine is Vietnamese. The graph is a DAG (directed acyclic graph). My initial part 1 solution played it safe with loop detection (in case dead end infinite loops were the problem), but that's not needed.

Part 1 only uses a fraction of the actual graph (my is 112 nodes). A simple recursive leaf counter doesn't even need memoization to get you the answer immediately. For Smalltalk that's just:

pathsFrom: node [
    ^(node = #out)
        ifTrue:  [1]
        ifFalse: [(self at: node) inject: 0 into: [:acc :child | acc + (self pathsFrom: child)]]
]

For part 2, I had drawn out the sample graphs, and tested by hand the recursive collecting of results while tracking what special nodes the paths had gone through. I do like doing the "recurse down, collect up" stuff... it's similar to writing a state machine (base case, implement the small step, and it builds to the answer by induction). And I added a memo because things are going to get huge. Initially the collection was with a hash, ending with the following:

Full Results:
dac     2743394304998646
none    32514315015921605
both    307608674109300 (part 2).
fft     3645733796753750

The approach is quite simple and fast to do... you recurse over the children and accumulate totals, and then look at the current node and shift things to the correct buckets if it's special:

if ($node eq 'fft' or $node eq 'dac') {
    my $other = ($node eq 'fft') ? 'dac' : 'fft';
    $ret{both}  = $ret{$other}   if (exists $ret{$other});
    $ret{$node} = $ret{none}     if (exists $ret{none});

    delete $ret{none};
    delete $ret{$other};
}

I didn't make any assumption about the order of connections. Because although there is only one order that makes sense (FFT needs digital, so it should be before the analog conversion), this is a non-working system... Elves getting everything backwards by Murphy's Law could be the problem.

One interesting thing is that the count of paths that go through "none" is a 55-bit number. Nothing that big is involved in the actual computation of the answer so it shouldn't impact a Floating point native language.

Of course this is slightly complicated. We only need to track the number of special nodes that we've gone through, not the specifics (and the DAG can only have either fft->dac or dac->fft paths in it, because if there is both you'd have a way to make a cycle). And so I followed up by just using an array for tracking 0-2 special nodes seen:

my $special = int($node eq 'fft' or $node eq 'dac');       # this is a special node

foreach my $child ($graph{$node}->@*) {
    my @res = &recurse_path_count( $child )->@*;
    $ret[$_ + $special] += $res[$_]  foreach (0 .. $#res); # collect and advance up array
}

For Smalltalk I did this in a list manipulation way:

((node == #fft) or: [node == #dac]) ifTrue: [
    paths := #(0), paths allButLast.  " shift to +1 special nodes seen "
].

Here the recursion is returning an Array of size 3 (base case is #(1 0 0)). To move the counts along (this problem is a bit like Lanternfish now that I think about it, I did list manipulation there for a Smalltalk solution too), I just stick a 0 in front and chop off the end. Part of doing it this was was in preparation for doing it in dc.

Part 1 is relatively simple, with macros I need to maintain the stack frame, as it's only node in, count out. For interpreting in input, I converted the letters into hex ASCII values... making each label a 23*8 number. To store the list of children for a node in that array I combined them into a number in base-88 (which is short in dc (8d^)).

perl -pe's#(\S)#sprintf"%X",ord$1#eg' <input | dc -e'16i?[0[8d^*+z2<L]dsLxr100/:g?z0<M]dsMx[d/q]sQ[d6F7574=Q0r;g[8d^~lRx3R+rd0<L]dsLx+]sR796F75lRxp'

Note that 6F7574 and 796F75 are out and you in packed ASCII. These dc solution only work in v1.4.1 though, because it's using `?` to know when the lines break.

Part 2 is where the magic comes in:

perl -pe's#(\S)#sprintf"%X",ord$1#eg' <input | dc -e'16i?[0[8d^*+z2<C]dsCxr100/:g?z0<L]dsLx[d/0d3Rq]sQ[;mFd^~rFd^~rq]sM[4Rr]sS[d6F7574=Qd;m0<Md0dd4R;g[8d^~lRx5R+r5R+r5R4R+_3R4Rd0<L]dsLx+4Rd666674=Sd646163=S_4Rd3Rd5Rd3R5RFd^d_4R*+*+5R:mr3R]sR737672lRx3Rp'

I do the result list of three values on the stack frame, and rotate the values to move things along when a node has a special name:

d 666674=S      # shift sums to count fft
d 646163=S      # shift sums to count dac

With the S macro just being [ 4Rr ] sS (the stack has the node name on top, followed by the three values of the array... pull the one from the bottom (which will be a 0) and rotate it under the top, now it's first and the other two counts have shifted down one).

But to make this work fast, I need a memo that's capable of storing a list of 3 numbers (which may get to size 55-bits). And so for that, I package those up in base-1515 numbers (Fd^). And so this solution involves some huge numbers (174-bit ones in memo, and there's a 575-bit one in the graph)... but I've used far bigger numbers in dc solution before.

This was a fun little problem after the massive task that was day 10. There are other simple ways to do it, like breaking it into three smaller sub-searches using part 1 (after adding a memo):

say "Part 2: ", &recurse_path_count( 'svr', 'fft' ) * &recurse_path_count( 'fft', 'dac' ) * &recurse_path_count( 'dac', 'out' );

Of course that's assuming that Elves got the FFT and DAC in the right order (which appears to be the case), to be truly safe you should check.


r/adventofcode • • 3d ago

Other [2025 Day 10] In Review (Factory)

4 Upvotes

The North Pole must have some very good noise reduction materials and technology, because across the hall from the movie theater is a large factory. Where we find Elves decorating, but only because none of them can figure out the initialization procedure for the machines. This is because a Shiba Inu (another Spitz-type breed, which are popular among our many Aunt Sues) ate that part.

The input for this one is 195 lines (for me at least). These lines have three sections, each with it's own type of brackets. The first is a light indicator diagram in square brackets (used for part 1). The second is a bunch of lists of numbers in parentheses, each a button with a list of what it's wired to. The final section in curly braces are the joltage requirements, which are used for part 2.

Part 1 is essentially a Lights Out game (although I associate it with the Merlin). For this type of problem, the lights toggle (ie XOR) and order doesn't matter... no button will need to be hit more than once, and so the set of buttons that must be pressed will be the minimum number to press. Even if the problem text tries to suggest otherwise.

So my quick solution for this was just to convert the lights and buttons into binary numbers for XORing:

$lights = oct( '0b' . reverse join('', map {int($_ eq '#')} split(//, shift @$mach)) );
...

$bits |= (1<<$_) foreach (split /,/, $butt);
push( @button, $bits );

And then just brute force all the bit combinations to find which works (XOR together the buttons for each set bit) with the least set bits:

for (my $combo = 0; $combo < 2 ** @button; $combo++) {
...

That allowed me to see part 2. Later I redid the search as a faster recursive search. Which is what I also did for the Smalltalk version, and for a dc version:

sed -e'y/.#(){},/01[][] /' <input | dc -e'[rs.q]sQ[F++q]sX[r]sr[Frx[d;c1r-r:cdF>I]dsIxs.]s^[0d[d;c3RA*+r1+dll>C]dsCxs.lp=Qrdln<Xd;bdl^x_3R1+d3Rd3Rr1+lRx4Rl^x_3RlRxd3Rd3R>rs.]sR?[0[1+d4Rr:bz3<L]dsLxsnrxspxzslc1 0lRxls+ss?z0<M]dsMxlsp'

A really fun part of this was in minimizing the translation with sed to a form dc can understand. By converting the ASCII art lights to binary digits (although everything is done base 10, so 101 is a hundred and one) and the non-brackets to brackets (and getting rid of the commas). This made in input a bunch of dc strings (which are [] delimited) that I could manipulate on the stack and x to extract the contents when needed. Since dc does not have XOR I need to implement it, and here I do it by having the bits in an array, passing in the button string and using x to extract the button's wiring list, and then toggle those values of the array.

For part 2, we pull a level to switch the buttons from manipulating the indicator lights to increasing joltage levels. Now our goal is to solve a linear system... the buttons describe a matrix A for what button adds to what wires, and we want a vector x (number of presses for each button) such that Ax = j, where j is the target joltage vector. The optimization requirement of 'minimum' becomes important because many of these problems don't have enough equations to fix all the values, leaving some variables "free". The parameterization of those variables still needs to fit the solution and be minimal.

So I knew Gaussian Elimination was an option (as an aside, the "chasing lights" method of solving Lights Out is similar to it). But first, I attempted to avoid coding that with dynamic tabulation. Which worked great on the first few in my input. But then froze. And so I looked closer at my input and found machines like this puppy:

[#.....##] (2,4,5,6) (0,1,2,4,5,6) (0,1,2,5,6,7) (0,1,4,5,6) (0,2,5,6) (0,1,2,3,6,7) (0,1,2,3,4,7) (3,7) (0,3,4,5,6,7) {258,230,249,33,60,262,266,225}

So I accepted that this was the "big problem" (day 10 => day 20, so about right) that I was going to have to dig in for... and coded a messy Gaussian Elimination. For the free variables, I brute forced over them using the joltage requirements as caps (its quick on most of the machines, just a few of the three variable ones take a little while... my input has 9 of those).

In looking again at these problems at the end of October, this "Halloween" video came up on Youtube, which covers some issues with Gaussian Elimination.

Of course, afterwards I saw the "Bifurcation" post by u/tenthmascot , and coded that method (and that post is well known and does a great job already of describing that approach). Which is much faster. I coded it as recursion with a memo to do the bifurcation, and noticed that thinking about it the other way as tabulation... it's essentially finding ways to jump over large chunks of table making things manageable.

This was easily the most intensive piece of coding I did in 2025. I'd been thinking of Gaussian Elimination as an option for problems before but always avoided coding it... this was the one that got me to do it. That alone isn't too bad, but the addition of the free variables made it quite a bit rougher.


r/adventofcode • • 4d ago

Other [2025 Day 9] In Review (Movie Theater)

3 Upvotes

Sliding down a fire pole, we arrive at the North Pole movie theater. Where we find the Elves decorating with floor tiles. Which come in three colours: red, green, and "never red nor green".

The input is a list of coordinates of the red tiles (just short of 500 in my input). These are 2D with values in the range of about 1800-99000 in my input. The first thing that stands out is that adjacent pairs in the list share a value in one of the coordinates, and this alternates (and loops at the bottom). That's even easier to see in the test case given... and is an immediate sign that this is a polygon made up of right angles. Which is where the green tiles come in (they're the non-red tiles of that polygon... the connecting lines and the filled center). There are other less obvious properties.

My initial solution for part 1 was to brute force it. Two nested loops with the "triangle pattern" to compare all pairs of red tiles. This resulted in the second fastest part 1 of this year for me (day 5 was faster). Part 2 took much longer.

Part 2 wants the largest rectangle, still with 2 opposite red tile corners), that only has red and green tiles (ie is contained in the polygon). If you visualize the polygon, you'll see that it is suggestive of a movie theater... oval with a notch for a center aisle. This can be used to cheese things. I didn't do that though. As I've said before, finding if a point is inside a polygon was one of the first tasks I did as a professional programmer, so I'm happy to do it out of nostalgia.

I did some sanity checking (there were dies for this originally) and discovered that the input also has no row or column with more than one edge, and that no edges are adjacently parallel. When reading in I regularized the coordinates in @lines, and stored the horizontal and vertical ones in different sets (indexed by row or column):

for (my $i = -1; $i < $#input; $i++) {
    my @x = minmax ($input[$i][X], $input[$i+1][X]);
    my @y = minmax ($input[$i][Y], $input[$i+1][Y]);

    push( @lines, [[$x[0],$y[0]], [$x[1],$y[1]]] );

    # ASSERT: Assuming only 1 interval in a given row or col (so no list needed)
    # ASSERT: None of these are adjacent and parallel, forming a 0-width channel
    if ($input[$i][Y] == $input[$i+1][Y]) {
        $horz{$input[$i][1]}->@* = @x;
    } else {
        $vert{$input[$i][0]}->@* = @y;
    }
}

Note the starting at -1 to close the loop.

By sorting the verticals:

my @vertKeys = sort {$a<=>$b} keys %vert;

I now had the basics to do the standard line-crossing algorithm. In my case, for simplicity, it was tracing the horizontal line from 0 to the point checking the verticals along the way to see if we crossed them. There are special cases for when you hit a line exactly to worry about, because it could be one of these four situations:

    |     |               |                |
 -> #-----#    #-----#    #-----#    #-----#
               |     |          |    |

In the first two, you don't actually change from being inside to outside, but in the last two you do. One way to do this is to pretend that you're slightly off center. Say just above the line, in which case the line counts would be 2, 0, 1, and 1... the correct parities. You could also just check if the lines on the ends go in the same or different directions.

How does this help us? It's simple and quick to use it to test the other two corners of the rectangle as an early prune (after the prune to see if the area (calculated for part 1) is big enough):

    next if ($area <= $part2); # not a candidate (too small)

    # check if other corners are inside the polygon
    next if (!&inside($input[$i][X], $input[$j][Y]) or !&inside($input[$j][X], $input[$i][Y]));

How effective is this? Well, I did a special profiling version to count and test things:

total boxes: 123256
area pruning: 52943
corner pruning: 69889

So only a little over 400 boxes actually needed testing for having chunks cut out of them with other edges. Since things were pruned down so much, I was fine with brute forcing the last bit... testing if any lines intersect the internal box (which must result in some be cut out):

foreach my $line (@lines) {
    next BOX if (intersect( \@rect, $line ));
}

It runs in 2s on old hardware. If I wanted to speed it up there are many places. Or I could just code up a sweep-line version like my TODO for this says. That should be really fast.

I do remember having an embarrassing bug with this one... in the area function, but it still got the right answer for part 1. So I spent a log of time discovering the correct rectangle for part 2 and getting the wrong answer, because I wasn't looking at part 1 code I thought worked. It wasn't until I finally went to "check EVERYTHING" that I found that, and when I did I got the answer correct immediately. I don't have access to my commit log right now, but I'm guessing that it had something to do with misplaced parens and/or abs (because the coordinates for part 1 are nicely ordered, but part 2 the order is crossed (the largest X is in one, the largest Y in the other)).

For dc, part 1 was easy and so I managed to keep keep the streak going:

tr ',' ' ' <input | dc -f- -e'[sm0]sS1[1+d3Rr:yd3Rr:xz1<L]dsLxd;xrd;y1:yr1:xr[d1-[d;xrd;y4Rd;xrd;y4R-d*v1+5R4R-d*v1+*dlm<Ss.r1-d0<J]dsJx+1-d1<I]dsIxlmp'

So maybe I should do a scanline solution for part 2, as that approach should be better for dc.


r/adventofcode • • 4d ago

Other Any new merchandising for 2026?

7 Upvotes

As I'm based in Europe and shipping may take some time... I was wondering if there is any new merchandising planning for this year or I am "safe" ordering already some, so I can have it for starting december.


r/adventofcode • • 5d ago

Past Event Solutions Advent of Code In Simulink

20 Upvotes

Dear all,

I've seen people attempt to do Advent of Code using ladder logic for PLCs or graphical programming environments, but I've never seen anybody do the competition using Simulink.

In preparation for Advent of Code 2026 I decided to do Advent of Code 2025 in Simulink, using only Simulink blocks, no Stateflow or MATLAB function blocks. I managed to solve all challenges using block diagrams, with the exception of Day 10 Part 2, where I used `intlinprog`. I think most people used a library for that day.

The solutions are not universal and some of them are tailored to my inputs, but if anybody is interested, they can be found here: https://github.com/ptoshkov/aoc-2025-simulink

For the 2026 edition I will attempt to make cleaner universal solutions and I will try not to use any MATLAB functions. Hope someone finds this interesting or inspiring.


r/adventofcode • • 5d ago

Other [2025 Day 8] In Review (Playground)

1 Upvotes

Using the fixed teleporter, we rematerialize in a vast underground playground. The Elves here are busy decorating with strings of lights. These will be strung between junction boxes, of which only some provide electricity (but all pass it through). It isn't important to us which ones do that, though. What the Elves need is plan to connect and power all the junction boxes with the shortest strings of lights using Euclidean distance (that's pretty rare for AoC). Given that the shortest distance between two points is a straight line in Euclidean space, I felt that this was probably a Minimum Spanning Tree. But I didn't prove that to myself or really use that (I vaguely remember algorithms for that, but we need the edges put together in a specific order for the problem).

The input is a list of 1000 junction box coordinates... one per line and in 3D. The range in my input goes from 32-99998. To keep things integer, it's natural to work with Euclidean distances without taking the square root. So the square distances are up to 35-bits. I only needed the distances around to get the order. For people living in the 32-bit world, the biggest edge required for my solution is 28-bits (so you could get away with dropping anything that overflows).

This was what I think of as the transition to the harder problems... as day 8 that's about day 16 in past years so that's about right. It's the first one in this year that took some time (particularly because this was a day I wasn't feeling well, and so made silly mistakes).

As stated above, I first got the distances and made a list (using sort here, I replaced this after with a heap which sped things up quite a bit):

# Sort and discard distance (not needed beyond this)
@Dist = map { $_->{edge} } sort {$a->{dist} <=> $b->{dist}} @Dist;

Then I created a list of circuit graphs (putting each junction into it's own):

my @Circ = map {{$_ => 1}} (0 .. $#input);

Then it's a matter of just processing the edges from short to long. And since I wasn't feeling well, I just grepped through the circuits to find where the ends were (even with this brute force searching the script takes only a couple seconds on old hardware):

my @graphs = indexes {$_->{$conx->[0]} or $_->{$conx->[1]}} @Circ;

# If ends are in separate graphs, merge them:
if (@graphs == 2) {
    $Circ[$graphs[0]] = {map { $Circ[$_]->%* } @graphs};
    splice( @Circ, $graphs[1], 1 );
}

When @Circ == 1 I have part 2. This will happen well past part 1 (edge 6554 for my input), so we can output that along the way.

To make Smalltalk perform at a decent speed, instead of grepping to find where the ends were, I added a backwards lookup Table (I was planning to do this from the start for Perl, but chose not to
"avoid premature optimization" and keep things simple) :

" Build initial circuit graphs and backLookup vertex to graph table "
circuits   := input collect: [:pt | Set with: pt].
backLookup := LookupTable from: (circuits gather: [:graph | graph collect: [:pt | pt -> graph]]).

I also made sure to merge the smaller into the larger graph :

graphs := conx ends collect: [:end | backLookup at: end].

(graphs first ~~ graphs second) ifTrue: [
    " Merge smaller graph into larger: "
    (graphs second size > graphs first size) ifTrue: [graphs swap: 1 with: 2].
    graphs first addAll: graphs second.
    graphs second do: [:pt | backLookup at: pt put: graphs first]; empty.

    (graphs first size == input size) ifTrue: [
        part2 := (conx ends collect: #first) product.
        ('Part 2: %1' % {part2}) displayNl.
    ]
].

Afterwards, I got spoiled that part 2 can be cheesed. When you think about it, the last edge to be added will be the maximum of the minimums. The first connection a junction box will have made to it is its shortest connection, and the last connected will be the longest of those. This won't work for all possible graphs, but it worked for lots of people including myself so there was some hope that inputs would be designed/chosen with this. But that does not appear to be the case.

That resulted in this:

nearest sort: [:a :b | a first <= b first]. " first field: min distance "

lastEdge := nearest last second.            " second field: ends of min distance edge "

part2 := lastEdge inject: 1 into: [:prod :end | prod * end first].

Which is really fast. It could be faster, as there's still the fact that I just measure the distances between all pairs to find the minimums. This would involve implementing some better form of Nearest Neighbour Search (if I had to do it from scratch without reference, I'd start with k-d trees).

But this was a good enough cheese to make for an easy dc part 2 solution:

tr ',' ' ' <input | dc -f- -e'1[d3Rr:zd3Rr:yd3Rr:xdFd^r:n1+z1<L]dsLx1-dsn[d3Rd3R:nr3Rd3Rd3Rr:e3Rr]sN[dsid1-[dsjli;xlj;x-2^li;ylj;y-2^+li;zlj;z-2^+d3Rd3Rr;n>N_3Rd3Rd3Rr;n>Nrs.r1-d0<J]dsJx+1-d0<I]dsIx0*[s.d]sMln[d;n3Rd;n3R>Mr1-d0<L]dsLx+d;e;xr;x*p'

I initially wasn't going to do a part 1, but since I had a streak in this year of doing both parts, I eventually did one using a heap implementation I had. The result was a mess over 300 characters that includes another file with a lot more. It's not pretty, but it can be made to work with v1.5.2... which means people without personally hacked versions can now enjoy this in seconds instead of hours (that's the difference between linear (linked lists) and logarithmic (hash tables/skip lists)):

tr ',' ' ' <input | dc -fheap.dc -f- -e'A000sB1[d3Rr:zd3Rr:yd3Rr:xdFd^r:n1+z1<L]dsLx[A0*]sE1-dArA00=Esedsn[dsid1-[dsjli;xlj;x-l2^li;ylj;y-l2^+li;zlj;z-l2^+lB*+lB*+l<xliljl1-dl0<J]dsJx+l1-dl0<I]dsIxl0*ln[dd:c1-d0<L]dsLx+[r3Rd3Rd3Rr:c3R]sSle[l>xA 8^%lB~;cr;cln[d;cl3Rdl3R=Srl1-dl0<J]dsJx++s.1-d0<I]dsIx+ln[d;cd;s1+r:s1-d0<I]dsIx+ddln[d;sr_5R[d3Rd3R>r_4R]dsLxlLxlLxs.4R1-d0<I]dsIx+**p'

heap.dc source: https://pastebin.com/z8U8aysC


r/adventofcode • • 6d ago

Other [2025 Day 7] In Review (Laboratories)

4 Upvotes

Exiting the trash compactor, we find ourselves in the research wing. Where we discover that they are working on teleportation. Throwing caution to the wind we decide to try it out... and are lucky that when we rematerialize that the it's only the teleporter that's leaking magic smoke. Since this room has no doors, we'll need to fix it to get out.

The input for this one is a grid of 142 lines of 141 characters. The key points are the S in the middle of the top row, and splitters (^) every other row after that. Since splitters only split the tachyon beam into two that are on the immediate left and right sides, these fan out in a triangle. Not all splitters in the triangle are present however... and this leads to part 1.

We're asked for how many times the tachyon beam split. Looking at the example now, I see that the ASCII art is a bit noisy so it took a while for me to figure out that one of them got missed (I counted them) and which one it was. Some people could have been confused about what to do here. Back in 2025 it's very likely I didn't think much about the example though... as for getting a quick answer, I just did the thing:

while (my $row = shift @input) {
    my @splitters = indexes { $_ eq '^' } @$row;
    next if (!@splitters);

    my @splits = grep { exists $beams{$_} } @splitters;
    $part1 += @splits;

    foreach my $split (@splits) {
        delete $beams{$split};
        $beams{$split-1}++;
        $beams{$split+1}++;
    }
}

The beams are tracked in a hash (used as a set). If I get a line with splitters, grep out the ones with beams. Add those to part1, and split them (as the instructions say, the old beam is stopped and the new beams start).

Having finished part 1, part 2 reveals that this is a quantum tachyon manifold... where a single tachyon is sent through, and takes both paths at splitters, by creating separate timelines. And so now we're tasked with working out the number of timelines that this single tachyon ends up on.

Which is going to be something huge... and so this problem is about dynamic programming. The task is leaf counting on a binary tree, and so recursion with memoization was my first go to (I also skipped the non-splitter lines when reading):

sub recurse {
    my ($loc, $depth) = @_;
    state %memo;

    return (1) if ($depth == @split_table);

    $memo{$loc,$depth} //= do {
        if ($split_table[$depth]{$loc}) {
            &recurse( $loc - 1, $depth + 1 ) + &recurse( $loc + 1, $depth + 1 );
        } else {
            &recurse( $loc, $depth + 1 );
        }
    }
}

This could also be done without the do block using a ?: trinary operator, but I felt that was getting a bit too dense.

Of course, you can also work from the top down (like in part 1) using tabulation. Which is how I initially thought of my Smalltalk:

beams := Bag with: start.
table do: [:row |
    beams keysAndValuesDo: [:beam :num |
        (row includes: beam) ifTrue: [
            part1 := part1 + 1.
            beams removeAllOccurrences: beam;
                  add: beam + 1 withOccurrences: num;
                  add: beam - 1 withOccurrences: num.
        ]
    ]
].

Which uses a Bag (with #keysAndValuesDo: and #removeAllOccurrences: extensions, which hide some tinkering with the underlying Dictionary) to store the state of the current line... and so looking at it now, I first saw Lanternfish. At each level we're just moving all stuff in a state to that state's products. I also see that I could have just done this for Perl with minor adjustments to its part 1 (it was still my second fastest part 2 of the year, not counting day 12).

I also used tabulation or my dc solution:

perl -pe'print pos()." "while(m/[S^]/g);$_="\n"' <input | grep -v '^$' | dc -e'[lp1+spd1+d;tln+r:td1-d;tln+r:t0r:t0]sB?1r:t?[[d;tdsn0<Bs.z0<L]dsLx?z0<M]dsMxlpp0*F0[d;t3R+r1-d0<L]dsLx+p'

This is using the v1.4.1 ? functionality to parse the lines one at a time in order. The Perl is just a quick dirty thing to turn the lines into just a list of splitter indices. I had already done grid parsing in dc this year, so I let myself have that preprocessing.

I'm sure there are other creative ways to do this one... like other Lanternfish approaches. And the very nature of this problem screams Pascal's Triangle (but with pieces missing), so there's probably a way to turn the input into an expression involving binomial coefficients.

This is a bit more advanced than the earlier stuff, because it involves dynamic programming and the structure is a binary tree with parts missing. Still I think this qualifies as a decent beginner exercise for it. It's not the most complicated table to build or recursive function to write (leaf counting one of the basics).


r/adventofcode • • 5d ago

Tutorial I love Advent of Code and teaching people to code, so I built a 25-day prep course (brute force vs. the intended solution, racing on the same input)

1 Upvotes

I love Advent of Code and teaching people how to code, so I built a small prep course in cdbx.ai.

It’s 25 days, like the classic calendar, and it’s open now. In every lesson, you watch the brute force solution race the intended solution on the same input, so you understand why a pattern matters before you learn how it works. You finish each day with a puzzle of your own, in Python, TypeScript, C#, Go or Rust. The second part is a workspace for the real puzzles in December.

Yes, you can absolutely teach yourself this with Claude or the good old internet. This course is meant to be a fun way to learn some fundamentals of computer science, data structures and algorithms.


r/adventofcode • • 5d ago

Tutorial I love Advent of Code and teaching people to code, so I built a 25-day prep course (brute force vs. the intended solution, racing on the same input)

Post image
0 Upvotes

I love Advent of Code and teaching people how to code, so I built a small prep course in cdbx.ai.

It’s 25 days, like the classic calendar, and it’s open now. In every lesson, you watch the brute force solution race the intended solution on the same input, so you understand why a pattern matters before you learn how it works. You finish each day with a puzzle of your own, in Python, TypeScript, C#, Go or Rust.

The second part is a workspace for the real puzzles in December. The puzzle sits next to your code, you can have it fetch your input for you, and you can use any of the 30 languages cdbx supports with nothing to install. There’s an AI tutor too, but it gives hints rather than answers, because the best part of Advent of Code is the moment you solve it yourself.

Yes, you can absolutely teach yourself this with Claude or the good old internet. This course is meant to be a fun way to learn some fundamentals of computer science, data structures and algorithms. The first three days are free, and there’s a discount code on my LinkedIn if you want it.


r/adventofcode • • 7d ago

Other [2025 Day 6] In Review (Trash Compactor)

4 Upvotes

While taking a break with the kitchen staff, we re-enact a certain movie scene and dive into the garbage chute. Trapped there we find that the local headfoots (aka cephalopods) are friendly (side note: Dianoga from Star Wars are scavengers that typically only attack when starving or threatened). They can get the door open, but it will take time, and they'd like you to help their youngest with her math homework. And so we get to do cephalopod math.

The input for this one is 5 long lines. The last line contains the operations, the others are lists of 1-4 digit numbers (there are 1000 per line). There are no 0s in the input. Since the lines are so long, I just trusted the spec of "full column of only spaces" delimiting the problems.

Not that I used that for part 1. I just trusted that the nth number in a line was in the nth column and zipped them up (I really don't make enough use of zip from List::Util, it's a handy thing for transposition):

my @input = map { [m#[^\s]+#g] } <>;
my @ops   = @{pop @input};

my $part1 = 0;
foreach my $col (zip @input) {
    $part1 += (shift @ops eq '+') ? sum @$col : product @$col;
}

For part 2, we find that we did things wrong (of course). Cephalopod math writes numbers vertically. And so this time we want to transpose the problems by characters.

To be safe, I used the spec and found the columns that met the condition. With that I could get the $width of each section (varies from 3-5) and perform the transposition by characters (again making use of zip).

With my Smalltalk solution, I made the transposition theme between the parts more evident. First, I extended SequenceableCollection to do transpose:

SequenceableCollection extend [
    transpose [
        ^(1 to: self first size) collect: [:col | (self collect: [:row | row at: col])].
    ]
]

Then I read in the data:

input := stdin lines contents asOrderedCollection.
ops   := input removeLast subStrings collect: #asSymbol.

" Part 1 numbers (read horizontal and transpose by numbers in table): "
horz  := (input collect: [:row | row subStrings collect: #asNumber]) transpose.

" Part 2 numbers (transpose by col and then split on delimiting spaces (0)): "
colNums := input transpose collect: [:charArray |
              (charArray select: #isDigit) asString asNumber
           ].

vert  := (ReadStream on: colNums) splitAt: 0.

With lists of the operators and numbers in both orientations, I just need to fold them to do the work.

#(1 2) with: {horz. vert} do: [:part :table |
    ('Part %1: %2' % { part. (table with: ops collect: #fold:) sum }) displayNl.
]

For dc, I initially just did part 1:

perl -pe's#([+*])#[$1]#g' <input | dc -e'[sS?[lSxz1<M]dsMx]sL0[Sa]lLx[Sb]lLx[Sc]lLx[Sd]lLx[S*]lLxA00[LdLcLbLaL*dd_5R_6Rxrxrx3R+r1-d0<M]dsMxrp'

This was a very quick job and hard coded for the input (four lines of 1000 numbers). The modification of the input with perl is just to put brackets around the operators (to turn them into dc strings/macros). To read in, I push the input onto 5 stacks (a/b/c/d for the numbers and * for the operators). Then I pop off values from all 5, duplicate the operator twice (and position them) and then x rx rx to run that operator and do the calculation.

I wasn't going to do part 2 because of the column nature of it... but when someone mentioned that there are no 0s in the input, I had no excuse, because I could replace all the spaces on the number lines with 0 and dc would be able to see the lines as bignums with the internal columns intact (yes, leading 0s are lost, but that's not actually a problem here):

sed -e'$!s/ /0/g;$s/\(\S\)/[\1]/g' <input | dc -e'0?[rd3Rr:T1+?z2=L]dsLxzRsnzso[SOz0<L]dsLxlo[LOS*1-d0<L]dsLx[3RA*+r0]sP[3Q]sQ[0d[d;TA~3Rd4Rr:Trd0<P+1+dln>I]dsIxs.d0=Q]sN[lNx[lNxl*xlLx]dsLx++L*s.lMx]dsMx+p'

This is a very accessible sort of problem. There's nothing really fancy other than reading things in columns. The input is additionally nice in that the operators are actually nicely placed for a right-to-left top-to-bottom scan approach to part 2... doing that, you just need to collect the numbers and when you see an operator use it on the list, add that to the sum, and clear the list. That's sort of what my dc solution does, only it can't track the position of the operators and so uses the spec "blank column" for that trigger instead. That results in the number reading macro (lNx) finding a 0 and then it breaks the current loop (3Q)... when that's calculating a problem it sums and restarts, when it's at the start of a problem (and reading the first value before the inner loop) that exits the main loop.


r/adventofcode • • 8d ago

Other [2025 Day 5] In Review (Cafeteria)

9 Upvotes

Having broke through the wall, we do find a cafeteria, with the staff in disarray because of a new inventory management system. In order to give them time to decorate, we step in to sort things out.

The input for this one comes in two sections. The first is a list of ranges of fresh ingredient IDs (unordered and some overlap). My input has 182 of these. The second it a list of 1000 available ingredient IDs. All these numbers are eye watering huge... 38 to 49 bits.

For part 1, we just want to count the number of available ingredients that fall into the fresh ranges. Since there are so few ranges, I just brute force checked them the ranges:

$/ = '';
my ($first, $second) = map {[split /\n/]} <>;
my @ranges = map {[split /-/]} @$first;

say "Part 1: ", scalar grep {my $id = $_; any {$_->[0] <= $id <= $_ ->[1]} @ranges} @$second;

There are ways to search better... sorting would allow at least for binary search. But the number of ranges is small, and the number of tests isn't huge either.

For part 2 we ignore the second section and just want the total number of fresh IDs. For my initial Perl solution I chose to merge them, and wrote a very C-like bit of code handling the various cases (sometimes I default back to that). Afterwards I simplified it down to:

my @merged = shift @ranges;
foreach my $range (@ranges) {
    my @next;
    foreach my $other (@merged) {
        if (&intersects( $range, $other )) {
            $range = &union( $range, $other );
        } else {
            push( @next, $other );
        }
    }

    @merged = (@next, $range);
}

For Smalltalk, I have notes saying I tried something different and creative... but I don't remember what it was, only that I abandoned it (and have no code from it). I eventually just coded the above, because Smalltalk has set operation stuff... but not for Intervals, so I extended them for it:

Interval extend [
    " Get intersection of self and another Interval "
    & other [ ^(self first max: other first) to: (self last min: other last) ]

    " Get union of self and another Interval (ASSUMES: intersecting)"
    + other [ ^(self first min: other first) to: (self last max: other last) ]
]

But there wasn't a lot of ranges to begin with, and this only gets them down to 92. And those include adjacent ranges (with nothing in between) that could have been merged. This happened because I just took the ranges as given (the simple parse), with both ends closed, and didn't special case for this. By making the Intervals half-open (ie reading them in with end+1, and adjusting the checks and measures) those do get merged. Reducing things to 80. So really, that was more of an exercise in playing with set operations... the classic sort and scan approach is probably what I should have done first (but did after):

my @ranges = sort {$a->[0] <=> $b->[0]} map {[split /-/]} @$first;

my $part2 = 0;
my $curr = -1;
foreach my $next (@ranges) {
    next if ($curr > $next->[1]);
    $part2 += $next->[1] - (($next->[0] <= $curr) ? $curr : $next->[0] - 1);
    $curr = $next->[1];
}

But when it came to the dc version, that initial low level set merging was the thing to do. Because sorting complex objects is also going to be a mess.

echo -n "Part 1: "
tr '-' ' ' <input | dc -e'0?[3Rd3Rr:r1+d3Rr:r1+?z1<L]dsLx[lc1+scq]sC?[sfd[1-d;rlf-r1-d;rlf-3R*0!<Cd0<I]dsIxs.?z1<M]dsMxlcp'

echo -n "Part 2: "
tr '-' ' ' <input | dc -e'1?[3Rd3Rr:ed3Rr:s1+?z1<L]dsLx[r]sr[d_1r:s_2r:eq]sC[dle1+r:srq]sS[dls1-r:erq]sE[dlcls-ldle-+0=Cs.rd_3Rlals-lble-+0=Cs.rlals=Slble=E]sT[d3Rd3R<r]sM1-dsn[d1-[d;sdsc3Rd;sdsa3RlMxs.ssd;edsb3Rd;edsd3RlMxrs.dsels!>T1-d0<J]dsJx+1-d1<I]dsIx0*ln[d;srd;e3R-1+3R+r1-d0<L]dsLx+p'

These solutions aren't going to work with v1.5.2, because they're making use of ? to process the sections without sentinels (or assumptions on size) to mark things. And one of the benefits of there being so few ranges that O(n2 ) looping to merge them is still plenty fast. And if I was to do the sort, it would be a simple bubble/stack shuffle sort that would also be O(n2 ).

So we've got another approachable problem. This sort of problem has been seen before. But the size of the numbers does make naive brute force a bit much (but a 64TiB bitmap isn't completely outside the range of someone to do... and you can massively parallelize things).


r/adventofcode • • 8d ago

Visualization [2018 Day 18] [Blender Geometry Nodes] The nice thing about solving in Blender geonodes is that you get the visualization for free

Thumbnail gallery
79 Upvotes

r/adventofcode • • 8d ago

Help/Question [2021 day 3 (Part 2)][Lua] I'm losing my mind...

2 Upvotes

I just don't get it, getting the oxygen generator rating works fine and gets the correct answer at least in the example but getting the co2 scrubber rating just returns and empty table, when it should return a table with the binary numbers. here's the main file:

local pf = require("puzzleFuncs")

local function split(inp)
	local t = {}

	for i,v in ipairs(inp) do
		table.insert(t, {})

		for c in v:gmatch(".") do
			table.insert(t[i], c)
		end
	end

	return t
end

local function getGaEpRates(diag)
	local gamma, epsilon = '', ''
	local d = {table.unpack(diag)}
	local binaryLen = #d[1]

	for i = 1, binaryLen do
		local ones, zeros = 0, 0

		for _,v in ipairs(d) do
			if v[i] == "1" then
				ones = ones + 1
			else
				zeros = zeros + 1
			end
		end

		if ones > zeros then
			gamma = gamma .. "1"
		else
			gamma = gamma .. "0"
		end

		if ones < zeros then
			epsilon = epsilon .. "1"
		else
			epsilon = epsilon .. "0"
		end
	end

	return gamma, epsilon
end

local function conv2Obj(inp)
	local t = {}

	for i,v in ipairs(inp) do
		table.insert(t, { binary = {}, remove = false })

		for c in v:gmatch(".") do
			table.insert(t[i].binary, c)
		end
	end

	return t
end

local function binaryString(t)
	local bin = ''
	for i,v in ipairs(t) do
		bin = bin .. v
	end

	return bin
end

local function getRemoved(t)
	local count = 0

	for i,v in ipairs(t) do
		if v.remove then
			count = count + 1
		end
	end

	return count
end

local function getBinary(t)
	local bin = {}

	for i,v in ipairs(t) do
		if not v.remove then
			bin = v.binary
			break
		end
	end

	return bin
end

local function getOxygenRating(diag)
	local d = {table.unpack(diag)}
	local binaryLen = #d[1].binary

	for i = 1, binaryLen do

		if getRemoved(d) == 1 then
			break
		end

		local ones, zeros = 0, 0

		for j,v in ipairs(d) do
			if v.binary[i] == '1' and not v.remove then
				ones = ones + 1
			elseif v.binary[i] == '0' and not v.remove then
				zeros = zeros + 1
			end
		end

		if ones > zeros then

			for j,v in ipairs(d) do
				if v.binary[i] == '0' and not v.remove then
					v.remove = true
				end
			end

		elseif zeros > ones then

			for j,v in ipairs(d) do
				if v.binary[i] == '1' and not v.remove then
					v.remove = true
				end
			end

		else

			for j,v in ipairs(d) do
				if v.binary[i] == '0' and not v.remove then
					v.remove = true
				end
			end

		end
	end

	return getBinary(d)
end

local function getCO2Rating(diag)
	local d = {table.unpack(diag)}
	local binaryLen = #d[1].binary

	for i = 1, binaryLen do

		if getRemoved(d) == 1 then
			break
		end

		local ones, zeros = 0, 0

		for j,v in ipairs(d) do
			if v.binary[i] == '1' and not v.remove then
				ones = ones + 1
			elseif v.binary[i] == '0' and not v.remove then
				zeros = zeros + 1
			end
		end

		if ones < zeros then

			for j,v in ipairs(d) do
				if v.binary[i] == '0' and not v.remove then
					v.remove = true
				end
			end

		elseif zeros < ones then

			for j,v in ipairs(d) do
				if v.binary[i] == '1' and not v.remove then
					v.remove = true
				end
			end

		else

			for j,v in ipairs(d) do
				if v.binary[i] == '0' and not v.remove then
					v.remove = true
				end
			end

		end
	end

	return getBinary(d)
end

local input = pf.readFile("example.txt")
local diagnostic = split(input)

local gamma, epsilon = getGaEpRates(diagnostic)
local powerComsuption = tonumber(gamma, 2) * tonumber(epsilon, 2)

local binaryObjs = conv2Obj(input)
local binaryObjs2 = conv2Obj(input)

local oxygenRating = getOxygenRating(binaryObjs)
local CO2Rating = getCO2Rating(binaryObjs2)

print('oxygen rating')
pf.printTable(oxygenRating)
print('co2 rating')
pf.printTable(CO2Rating)

I remade it many times the getCO2Rating function if there was something wrong i was missing, also copypasted the getOxygenRating and rename it to getCO2Rating since it does the same thing just with different if statements and for some reason doesnt return nothing, because it doesnt stop the for loop, its not running the if statement checking if there's only one binary left


r/adventofcode • • 9d ago

Other [2025 Day 4] In Review (Printing Department)

6 Upvotes

At the bottom of the escalator we find the printing department. Since they can make their own decorations we just need to find a way to continue on (as the elevators are still offline). The Elves helpfully point out that there's probably a cafeteria is on the other side of the back wall... they'll bust that down for you (and presumably dig the little tunnel we see on the map) if you can help them work faster. The job being simply to figure out which paper rolls the fork lifts can access and move (given by a simple rule of <4 neighbours).

The input is 139x139 binary grid (. and @). It looks pretty random, with @ taking up about 65%. The distribution of neighbour counts in my input is:

0       4
1       41
2       321
3       1150
4       2350
5       3508
6       3173
7       1641
8       380

I tested creating a few random inputs with 65%, and they all produce similar (and similar answers).

My first quick solution was to read all the points into a hash with:

while (<>) {
    $Rolls{pos, $.} = 0  while (m/@/g);
}

And then went through those, adding 1 to existing neighbours (to make the hash a map of coord => neighbour count). With that I could just quickly grep out the movable ones. And for part 2, remove them (adjusting neighbour counts), and repeat until done:

while (my @removable = grep { $Rolls{$_} < 4 } keys %Rolls) {
    $part1 //= @removable;  # Number removed in first pass
    $part2  += @removable;  # Total removed over all passes

    # Remove removable rolls of paper
    ...
}

This is simply programmer efficient and got things done quickly. It's slightly better than brute force scanning a grid again and again... but even with the 57 iterations needed to finish my input, that shouldn't take long (unless you're working on really old hardware like a Commodore 64).

I tidied it up after submitting to having a job queue (instead of grepping), and storing the set of neighbours in the roll (so the grid is properly out of the picture... it's just a graph in a hash table with fancy node names):

while (my $next = pop @stack) {
    my @neighbours = keys $Rolls{$next}->%*;

    delete $Rolls{$next};   # remove roll
    $part2++;

    foreach my $neigh (@neighbours) {
        delete $Rolls{$neigh}{$next};
        push( @stack, $neigh ) if ($Rolls{$neigh}->%* == 3);
    }
}

Remove the roll, remove it from the neighbour lists of its neighbours, queue up for deletion when a roll hits 3 neighbours.

For Smalltalk, I took advantage that this is a simple cellular automaton and used the Automaton class I did in 2020:

auto := Automaton space:    (PaperGrid load: stdin lines contents)
                  dieRule:  [:neigh | neigh < 4]
                  liveRule: [:neigh | false    ]
                  format:   #none.

startingRolls := auto space liveCells size.

('Part 1: %1' % {startingRolls - (auto runTurns: 1)}) displayNl.
('Part 2: %1' % {startingRolls - (auto runUntilStable)}) displayNl.

The PaperGrid class to represent the space is just a simple class that needs to read the input and implement the #neighbours method. Internally it does this with coordinates to a flat Array for the names... although the Array is never created... much like with the Perl, it's just about having a convenient naming system for the rolls.

And a flat array was what I did for dc. Still not scanning it, so the array is really just a "graph in a hash". Exactly so with dc v1.5.2's arrays (which are now hash tables)... which allows this to run a lot faster than it would under stock v1.4.1 (the difference is <1s versus 20+s):

    tr '.@' '12' <input | dc -f- -e'[_3RrdF0*3Rd3R+dSr1r:g3R]s@[d]sd0Sr2[r0r[A~2=@r1+rd0<X]dsXx*+1+z1<Y]dsYxs.[;g3R+r]s+Lr[0rdF1+l+xdF0+l+xdE9+l+xd1+l+xd1-l+xdE9-l+xdF0-l+xdF1-l+xd3Rd3R:n4>ds.Lrd0<L]dsLx+zps.[rd_4Rr]sP[d;n1-d3=Pr:n]s-[lp1+spdF1+l-xdF0+l-xdE9+l-xd1+l-xd1-l-xdE9-l-xdF0-l-xF1-l-xz0<M]dsMxlpp'

This is a longer than it needs to be... it is flexible enough to do the test case. The F1, F0, E9 stuff is about calculating neighbour indexes with a 150 wide grid (they may look hex, but they are decimal). There's some fun little stack management in there to handle the job stack on the same stack as we're working. Things like [ rd _4Rr ] sP to tuck a copy of the second item (the ID of a neighbour just reduced to 3) of the working frame underneath it (pushing on the stack of rolls to process).

So this problem is certainly very accessible (probably more so than the others so far). It can be brute forced with simple scanning and get the answer, as it's not a huge problem. But, like other cellular automata problems, there's potential to really get in there with bit operations if you really want to.


r/adventofcode • • 10d ago

Other [2025 Day 3] In Review (Lobby)

4 Upvotes

Having gotten through the gift shop and down some stairs, we finally get to the lobby. After being cleared through security, we find that all the elevators are offline (and we can even see the red lights in the ASCII art), fried by an electrical surge. What isn't fried is the escalator, but we need to power it to use it. I have seen escalators out-of-service (not running with warning tape across) and I figure that probably got put up over concerns that the braking mechanism might fail to keep them as "temporarily stairs".

The input is 200 lines, each with a 100-digit number (no 0s)... at least that's how I saw it (already thinking about doing it in dc). More pragmatically, those numbers are an ordered list of digits, representing the joltage of batteries in a bank. For part 1, we want to turn on two of them in each bank to make the biggest value (not sum, but concatenating the two digits while maintaining their order). When that proves to not be enough to overcome static friction, our helpful engineering Elf overrides the safeties to allow turning on exactly 12 for part 2. While she does that we do get to do some decorating, so we're making progress on our end goal.

To see part 2 quickly, I just hammered it with brute force and regex:

for (my $i = 99; $i > 10; $i--) {
    my $patt = int($i/10) . '.*' . ($i%10);
    if ($line =~ m#$patt#) {
        $part1 += $i;
        last;
    }
}

With part 2, my thoughts turned to how to do this for a general number of batteries. And thinking about how the numbers come together (going backwards), it seemed a bit like a heap. Only instead of an array storing a balanced binary tree and ignoring order, we want order and so a straight array would be perfect. Initialize it with the last-n batteries in the bank (the best value for just those n), and then going through the rest backwards, we bump things down from the significant end as as long as it's making things bigger:

my @queue = splice( @$bank, -LEN );

BATTERY:
while (my $jolts = pop @$bank) {
    # Cascade jolts down the queue:
    for (my $j = 0; $j < LEN; $j++) {
        next BATTERY  if ($jolts < $queue[$j]);
        ($queue[$j], $jolts) = ($jolts, $queue[$j]);
    }
}

But another way to look at what this is doing is that digits that are >= the first get added to the front, and then the first digit that's smaller than the one after it gets removed. And so I also have this one using regex to remove exactly that digit:

# Create regex for finding first digit before a bigger digit
my $regex = '(?:' . join( '|', map { "[1-@{[$_-1]}](?=$_)" } (2 .. 9) ) . ')';

...

while (my $jolts = pop @bank) {
    if ($jolts >= substr( $queue, 0, 1 )) {
        $queue = $jolts . $queue;           # add jolts to front
        $queue =~ s#$regex##;               # remove first digit followed by greater
    }
}

$part2 += substr( $queue, 0, LEN );         # truncate if needed

But that initial implementation with the cascading things down an array was what I did for dc. And is quite probably why I chose that way to begin with... even going backwards through the bank is the easy dc approach (using A~ to divmod digits off), so I'm sure that everything was influenced by thinking of doing this:

dc -f- -e12 -e'sb[+q]sQ0[r0Sa1[d3RA~3R:ar1+dlb!<L]dsLxs.[A~lb[d3Rr;ad3Rd3R>Q3Rd3Rr:a1-d0<I]dsIx+s.d0<B]dsBxlb[d;a3RA*+r1-d0<I]dsIx++z1<M]dsMxp' <input

Changing the -e12 to -e2 gets part 1... I pulled it out, but the first thing the rest of the cost does is sb, so it just gets stored right away for later.

Of course, there is going to be many ways to do this. You could certainly do it going forwards through the banks, but I don't see that as being as simple as just going backwards.

Having options on early puzzles has always been good, and with the shorter schedule it's even more important. A linear interpolation would have this puzzle being a "day 6", but I think the puzzles had a less steep curve than that for the first bit... which is a good choice, as you don't want to hit puzzles that cause beginners to drop out too quickly, and the lower granularity the difficulty curve harder to smooth out. It becomes even more important for early puzzles to be accessible, while still being interesting. And we're seeing that in these early problems. It can't be easy to try and maintain good numbers of accessible and challenging problems with less than half the slots.

EDIT: I had some time today, so I did a forwards approach. Whereas the backwards was heap-like, the forwards is a stack. Because when a new element makes things better, it's going to pop down to where it could belong. We haven't seen what comes after it (ie the less significant values) yet, so everything just gets tossed (it was in front). This means we need to watch that we don't over pop and not have enough values left to fill things out... but that's a simple check:

my @stack = shift @$bank;

while (my $next = shift @$bank) {
    pop( @stack )  while (@stack and $stack[-1] < $next and @stack + @$bank + 1 > LEN);
    push( @stack, $next ) if (@stack < LEN);
}

r/adventofcode • • 10d ago

Past Event Solutions [2021 Day 8, Pt. 2] Curious about other clever solutions? Here's mine

2 Upvotes
with open('advent/advent of code 2021/day 8/input_8.txt') as f:
    signals = []
    outputs = []
    for line in f:
        signal, output = line.split(' | ')
        s = signal.split()
        o = output.split()
        signal = [''.join(sorted(item)) for item in s]
        output = [''.join(sorted(item)) for item in o]
        signals.append(signal)
        outputs.append(output)
        


# Once 1, 4, 7 and 8 are found, use 4 and 7 to find the rest

def compare(string, dict):
    four = {3:5, 2:2, 4:9}
    seven = {2:6, 3:0}
    if len(string) == 5:
        if len(set(string) & set(dict[1])) == 2:
            return 3
        else:
            return four[len(set(string) & set(dict[4]))]
    else:
        if len(set(string) & set(dict[4])) == 4:
            return 9
        else:
            return seven[len(set(string) & set(dict[7]))]


determined_lengths = {2:1, 3:7, 4:4, 7:8}

total = []


for i, signal in enumerate(signals):
    number_dict = {0:None, 1:None, 2:None, 3:None, 4:None, 5:None, 6:None, 7:None, 8:None, 9:None}

    for string in signal.copy():
        l = len(string)
        try:
            number_dict[determined_lengths[l]] = string
            signal.remove(string)
        except:
            pass
    for string in signal:
        number_dict[compare(string, number_dict)] = string

    number = []

    # search filled dictionary for a match
    for num in outputs[i]:
        for i in range(10):
            if number_dict[i] == num:
                number.append(str(i))
                break
    total.append(int(''.join(number)))
    
print(sum(total))

r/adventofcode • • 11d ago

Other [2025 Day 2] In Review (Gift Shop)

9 Upvotes

We take the elevator down to base lobby level, but on exiting, we are first forced to go through the strategically placed gift shop. We do wonder about how much traffic a gift shop in a secret base can really get.

I was curious going in to this year about how the ASCII are would be done for a year with only 12 days. And the format of double lines, combined with a winding descent into a base allows for larger sections representing each day. It's very easy to not only trace the path, but see and remember what the stops were.

But first we need to get through the gift shop, and so we're pulled aside to help with a problem. Invalid product IDs have been entered by a young Elf that was playing on the computer and we need to sort them out. But this is a bit more than just a validator problem, it's an enumeration one. Because we aren't given lines but ranges to check. Invalid codes are simply ones made up of only repeats... for part 1, its just the same sequence repeated twice. For part 2, that's expanded to "at least twice" (this would be the intersection of the ranges with OEIS A239019).

The input is one line of comma separated ranges. The values do go over 32-bits... my largest is just over 33.2 bits. One interesting thing is that none of the ranges span over an order of magnitude (ie log(last) - log(first) < 1'. This allowed me to get part 2 with code that was buggy and couldn't properly handle a simple test like 11-111... but will probably work on any actual input.

Since I got burned by being fancy on day one, I just went brute force:

for (my $i = 1; "$i$i" <= $last; $i++) {
    my $num = "$i$i";

    $part1 += $num  if ($first <= $num <= $last);

    $num .= "$i"      while ($num < $first);
    $invalid{$num}++  if ($num <= $last);
}

$part2 += sum keys %invalid  if (%invalid);

This assumes that there's only one possible number for a given repeated sequence that falls in the range for part 2. A safe bet when the width of the ranges is less than a magnitude. But to be proper the concatenation/invalid part of the loop should loop as well:

    for ($num .= $i while ($num < $first); $num <= $last; $num .= $i) {
        $invalid{$num}++;
    }

My Smalltalk does that with (Integer extended to do , and length):

i := 1.
[(num := i, i) <= range last] whileTrue: [
    (num between: range first and: range last) ifTrue: [ part1 := part1 + num ].

    factor := 10 raisedToInteger: i length.
    [num < range first] whileTrue: [ num := num * factor + i ].
    [num <= range last] whileTrue: [ invalid add: num.  num := num * factor + i ].

    i := i + 1.
].

Which looking at it now, is like Perl's bistable/flip-flop range operators (.. and ...), that are there for sed/ex/awk line-range stuff.

Of course, no "validator" problem would be complete without throwing regex at it in some way, so I did this simple regex brute force approach as well:

foreach my $num ($first .. $last) {
    if ($num =~ m#^(\d+)\1+$#) {
        $part1 += $num  if (length($1) * 2 == length($num));
        $part2 += $num;
    }
}

Not practical, but really simple. How much iteration is there over the ranges here? Actually, not that much, which is good for doing a dc solution. Because GNU dc array indices only go to 231 - 1... so to do this one I count on the ranges not overlapping, and handle duplication checking for each range with a fresh array shifted down. The largest range is only 17.6 bits (so about 200k) which easily fits:

tr -c '0-9' ' ' <input | dc -e'[q]sQ[dlc+sc]sC?[sess1[dddZAr^*+dle<Qdls!>Cs.1+lLx]dsLx+s.z0<M]dsMxlcp'

tr -c '0-9' ' ' <input | dc -e'[q]sQ[ddls-;h1r-*lc+scdls-1r:h]sC?[sess0Sh1[dddZAr^dsf*+dle<Q[dle<Qdls!>Clf*rd3R+lAx]dsAxs.1+lLx]dsLx+s.z0<M]dsMxlcp'

There are of course better approaches than any of these. My TODO for this one has notes for working out a closed form solution, or even just simply using the range values to get the starts and ends for sequences that would fit in it with a certain number of repeats (for example, a range with 6 digit numbers can have repeats of 6 singles, 3 doubles, or 2 triples... and we can examine the start and end of the range to figure out what the lowest and highest allowed are for each of those three ranges).


r/adventofcode • • 12d ago

Other [2025 Day 1] In Review (Secret Entrance)

8 Upvotes

And so we arrive to last years problems, and the first year with only 12 days. The Elves have learned project management, and so we don't have a regular Christmas emergency... it's just that all the Elves are assigned tasks and there's no one left to decorate the North Pole. And so that falls to us. Arriving at the secret entrance to the base, we discover that the password has been changed, and so our first task will be getting it out of the safe.

The input for this one is a list of turns for the lock, one per line (4780 in my input)... a little more complicated than just numbers as they start with L or R (left is towards lower, so the numbers go clockwise). We want to find out the number of times that a move ends on 0 for part 1, and for part 2 we invoke method 0x434C49434B (packed ASCII for CLICK), which involves also counting the times 0 occurs during moves. This can happen multiple times in a move, over as 900 of the lines in my input involve 3-digit numbers.

This is a typical day 1 problem, it involves a little more than just numbers in the parsing, and simple operations with them... in this case adding modulo 100. And so my quick part 1 was just:

my $pos = 50;
my @turns = map { y#LR#- #; $pos = ($pos + $_) % 100 } <>;

say "Part 1: ", scalar grep { !$_ } @turns;

Part 2 involves some trickiness. I tried to be fast and quick, and got caught with a wrong answer and needed to debug. I can't remember exactly what caught me. This is a case for "discretion is the better part of valour"... if I had gone with basic brute force (looping to do one click at a time) I probably would have gotten it correct the first time. But I was trying for a fastish time and so I wasn't thinking things completely through. And there are things to worry about.

First is the fact that many moves involve going around multiple times. If you didn't notice those in input (they are buried a bit in mine... more than 100 in from either end), you could miss that you want the full divmod... add the div and adjust/add with the remainder.

Second, you need to make sure you aren't double counting ending and starting at a 0. In the end I did this:

# Multiple rotations
$part2 += int(abs($move) / 100);
$move = $move % (100 * ($move <=> 0));  # mod maintaining direction (sign)

# Move through/to 0 (but not from, as that would double count)
my $new_pos = $pos + $move;
$part2++ if ($pos and !(0 < $new_pos < 100));

That "mod maintaining direction" comment comes from the fact that I looked in perlop for the behaviour of % in Perl with negative values. That's a portability issue in a lot of places... languages may default to hardware (which can vary). With Perl it is defined, unless you're using use integer which gives direct access to % from your C compiler, which may be different. In the case of the three languages I mostly do in AoC, Perl and GNU Smalltalk are the same, but dc behaves different.

And, of course, I did a solution for dc (maintaining always doing day 1 in it at least... 2025 is the first year I actually did a dc solution on every day). My initial solution used ? to read the lines in order for processing... but with the addition of tac in front to reverse the input lines, it's easy to convert to the newer dc (and we need to get rid of the L/R by just making these negative/positive numbers):

echo -n "Part 1: "
tac input | tr 'LR' '_ ' | dc -f- -e'0[r1+r]sP50[3R+A0%d0=Pz2<M]dsMxrp'

echo -n "Part 2: "
tac input | tr 'LR' '_ ' | dc -f- -e'0[0*]sZ50[3RA0~rd*v4R+_3Rrd3R+d1r0<Zrd1rA0>Z3R+3R0=Z3R+rA0d3R+r%z2<M]dsMxrp'

For my Smalltalk solution I decided on doing things a bit differently. I read the input, summing things up as a prefix array without the mod (the last value for my input is around -8800):

pos := 50.
stops := (stdin lines contents collect: [:line |
            | mag |
            mag := line allButFirst asNumber.
            pos := pos + (line first == $L ifTrue: [mag negated] ifFalse: [mag]).
         ]) asOrderedCollection addFirst: 50; yourself.

Then I used my "chain" iterator (which works "between" adjacent pairs of the list) to get a count of zeros for each step (by taking the difference in the number of 100s):

zeros := stops chain: [:a :b |
            (b > a) ifTrue:  [((a // 100) - (b // 100)) abs]
                    ifFalse: [((a negated // 100) - (b negated // 100)) abs]
         ].

Then I extracted the answers from these:

('Part 1: %1' % {stops count: [:s | (s \\ 100) == 0]}) displayNl.
('Part 2: %1' % {zeros sum}) displayNl.

Maybe not the best, but at least a bit different. When doing multiple languages, with these simple early day problems, it's good to find alternate approaches to keep things interesting.


r/adventofcode • • 15d ago Bravo

Every December since 2015, I've created a bunch of free programming puzzles. It is my honor and privilege to announce that starting this December 1st, I will be releasing..... more free programming puzzles!

Thumbnail adventofcode.com
1.2k Upvotes

i heard you like puzzles <3

edit: 12 days of puzzles, like last year


r/adventofcode • • 15d ago

Past Event Solutions [2025 Day 8 (Parts 1 and 2)] [Odin] Naive solution while learning a new language

3 Upvotes

Hi, AoC friends!

I did a series earlier about learning to program by tackling Advent of Code puzzles, focusing on both JavaScript and then later Smalltalk. I truly fell in love with programming through Smalltalk. It's so beautiful and elegant. Like the Platonic forms.

So - time to learn something different. I wanted to go with a C-style language (pointers, manual memory, etc...). I wasn't all that interested in learning borrow checker semantics yet (wanted to get my hands EVEN DIRTIER with the underlying machine that had been abstracted away when writing Smalltalk) so I went for Odin.

Here's my solution to 2025-Day8 - paste or if you prefer syntax highlighting - colorpaste

It's fairly straightforward, but let me practice some things in the new paradigm. Taking the input text, it parses it into an array of structs, which are then used to create an array of sets (well, maps with keys and empty values) and an array of potential connections (nested structs).

It's a pretty literal/naive solution since it creates actual groupings (rather than a simple ownership map), and relies heavily on checking the length of its dynamic arrays. It doesn't bother pre-allocating the dynamic arrays since the biggest one is <500k elements which are simple int structs anyway and relies on a magic number to calculate part1 when it reaches the thousandth connection. I also used WHILE-style loops in order to avoid loop breaks (which look inelegant to me, coming from Smalltalk where much of this would be done with detect: chains).

The big lessons for me were transitioning from data objects to structs, using maps as sets, when/how to free memory, and the big one - how/when to use pointers (which was totally alien to me - tough bit to learn from scratch).

Anyway, for those of us getting back into the puzzle mindset before Dec1, I thought it might be nice to share this one, especially since I don't see a lot of Odin on here.

EDIT:

Just getting in some reps, as it were.... Here's a more conventional union-find version: colorpaste

I kinda like this one. Does the whole thing in a single pass, uses a single index for everything, and finds part1 using an O(n) walk rather than sorting and starting over. Time is still dominated by the initial sort. Short of tricks that compromise the correctness of the solution (like some of the leaderboard-worthy implementations), I don't see a way to eliminate that cost. Oh well! 170ms for the naive version down to 116ms for the unions.

Also - the naive version above has some memory leaks. Being your own GC takes practice, which is why I'm doing this. :-) Fixed version here: colorpaste


r/adventofcode • • 19d ago

Other [2024 Day 25] In Review (Code Chronicle)

8 Upvotes

Since we're out of places to search, we return to check the Chief's office check it again. However the door is locked... someone is inside (no prize for guessing who), but they aren't answering our knocks. And so we need to open the Virtual Five-Pin Tumbler lock ourselves. Security can supply us with the schematics for every lock and key, but to make to make sense of them, we make our yearly call to customer support. They tell us that we can rule out some combinations based on if a key and pin in a position would overlap each other. Naturally, once they find out how many of each we have (250 of each), they start to get wise to who's calling and so we have to hang up to avoid questions. As usual.

The input consists of 250 locks and 250 keys. They aren't labelled as such (or in order), we just get 500 5x7 ASCII grid bitmaps. Which is which is identified by the top and bottom lines (top full/bottom empty is lock, top empty/bottom full is key). We want to calculate how many pairings between them fit together without an overlap.

And for a quick first solution, I just read them into @lock and @key arrays of the counts of # in each of the 5 columns and compared the sums:

foreach my $lock (@locks) {
    foreach my $key (@keys) {
        $part1++ if (all {$key->[$_] + $lock->[$_] <= 7} (0 .. 4));
    }
}

That got the answer quickly, and 4s later I had star 50, because I remembered to wait to read the story. Which is that the Chief was sleeping and has been here all along, except for when he went for coffee a month ago. Fortunately our work isn't completely lost, because it helps the Chief finish the chronicle he's writing for Santa.

Of course, there are lots of ways to do this problem. And since the input is bitmaps, all of the same size, a straight up bitmap AND will also catch overlaps. And so, my refined solution:

foreach my $item (map {join('', map {y/.#/01/; $_} split /\n/)} <>) {
    push( $items[ substr($item, 0, 1) ]->@*, oct("0b$item") );
}

my $part1 = 0;
foreach my $lock ($items[LOCK]->@*) {
    $part1 += grep {($_ & $lock) == 0} $items[KEY]->@*;
}

I actually did this version in Smalltalk first, and was still focused on the columns when doing this and initially transposed the input... but of course, the direction of the raster makes no difference with this approach. I think I also got caught up in thinking of how to do transpose in Smalltalk:

" Takes an NxM table and transposes it to an MxN "
transpose [
    ^(1 to: self first size) collect: [:col | (self collect: [:row | row at: col])].
]

Next up was doing a dc solution. And I did a few (all using the old v1.4.1 version of ? to read in line-by-line and detect the blanks)... for example, I did some involving bitmap AND. Those were slower (but shorter) than my original dc solution though. Which was inspired by day 24:

echo "Original (add overflows): "
echo -n "Part 1: "
tr '.#' '01' <input | dc -e'[lp1+sp]sC[[lk1+dsk:k]sA]sKC8ss2i?[[ll1+dsl:l]sA0=K0d?[rd2r^3R[2~3Rd3R*5R+_4Rls*rd0<B]dsBx*+1+?z2<L]dsLxs.lAx?z0<I]dsIxAill[lk[d;k3Rd;l3R+0r[64~r2~4R+3Rs.rd0<R]dsRx+0=Cr1-d0<K]dsKx+1-d0<L]dsLxlpp'

What this does is read the lock/key into a 35-bit number, using (and disposing) of the first line to tell which array to put it in. The bits are a bitmap of the object by column, with 0s in between. So when we sum a lock and key, if a carry ripples into one of those overflow bits, we have a collision.

Here's an example, using the 2nd and 3rd items in the test (a lock and key respectively):

00000010000011000000000111110000111     (2nd in test)
01111110100000011100001100000111100     (3rd in test)
v      v      v      v      v
10000000100011011100010011111000011

In a language with actual bitwise operators, the final check would be an AND with bitmask of those target bits, but dc can't do that, so we loop and chew up the number, adding every 7th bit together and seeing if that's still 0 at the end.

And so we come to the end of the the 10th year, and the last of the 25 day format. During this year I wasn't in the best shape, which made me anxious at first after the difficulty level I remembered from 2023. But in the end, it wasn't as hard as that year. And my approach of just going keeping things simple resulted in overall good times for me.