Advent of Code 2024

Day 19 was actually pretty easy because it was similar to past puzzles.

Day 19 spoiler (both parts)

This only take a short recursive method that runs through the array of towels and compares them to the start of the string. When a match is made, it sends the remainder of the string to itself. Once the string is empty, a pattern was found. If no match can be made, no pattern was found.

The key is to cache each substring and its result after it’s tried.

if ss = "" then
  return 1
end if

if cache.HasKey( ss ) then
  return cache.Value( ss )
end if

var result as integer

for each a as string in available
  if ss.BeginsWith( a ) then
    result = result + Can( ss.MiddleBytes( a.Bytes ), available, cache, getAll )
    
    if result = 1 and not getAll then
      exit
    end if
  end if
next

cache.Value( ss ) = result

return result

I’ve been having a terrible time with Day 19. I had the code done last night in about 10 minutes and working with the example data, but when I ran it with the actual data, my recursive matching routine got stuck in an endless loop on certain patterns. I’ve never seen anything like it where a recursive routine gets stuck in a loop with no stack overflow. Calling parameters get smaller with each call but the function never completely unwinds. Eventually, I added a watchdog counter to kick it out after a large number of calls and assume a failed match in that case. That gave me the correct result for part 1, but I still have no idea what the problem was.

I’ve now rewritten my code for part 2, simplifying it quite a bit. Again, it gives the correct part 2 answer for the example data, but in the actual data there still appear to be some problem patterns.

Day 20 is easier than it first seems.

Day 20 spoiler

Since the maze has only one path, this isn’t an A* problem, it’s purely math.

First, get every point along the path in an array. Work forwards in that array (from Start to End - 100) and compare to every point starting at the end of the array (End to the index you’re considering). If the Manhattan distance of that point is 2 in part 1, or between 2 and 20 in part 2, subtract the second index from the first to get the savings, then subtract the distance between the points. If that number is greater than or equal to 100, increment your count.

Private Shared Function Solve(path() As Xojo.Point, limit As Integer, distance As Integer) As Integer
  var lastIndexToConsider as integer = path.LastIndex - limit
  
  var count as integer
  
  for upIndex as integer = 0 to lastIndexToConsider
    var lowPoint as Xojo.Point = path( upIndex )
    
    for downIndex as integer = path.LastIndex downto upIndex
      var highPoint as Xojo.Point = path( downIndex )
      
      var thisDistance as integer = M_Path.ManhattanDistance( lowPoint.X, lowPoint.Y, highPoint.X, highPoint.Y )
      
      if thisDistance >= 2 and thisDistance <= distance then
        var savings as integer = downIndex - upIndex - thisDistance
        
        if savings < limit then
          continue
        end if
        
        count = count + 1
      end if
    next
  next
  
  return count
End Function

In the IDE this takes about 15s. Compiled, around 1s.

Yeah, I think I know how to approach Day 20 (I didn’t look at the spoiler yet).

I’ve realized that rather than diving in immediately after they’re posted, if I wait a while and think about it, the solution becomes apparent, and saves a lot of wasted time. Unfortunately, I’m getting swamped with other things now, and don’t know if I’m going to be spending much time on these for the next few days.

Day 20 spoiler

Similar to Kem’s method.

First, traverse the grid to generate the running step count (aka picoseconds) for each point on the path, and store them in an array.

Then, for the part 1 solution, because the cheat move was only two steps, I simply looked at each internal wall element and took the difference in path length values on opposite sides of each of these wall elements. This was quick to code and ran quite fast, but wouldn’t work for part 2 where the cheat distance was a range of 2…20.

So for part two, I generated an array of all possible x,y offsets for distances 2…20. (It’s general enough that by setting variable nCheatMax to either 2 or 20, it solves both parts 1 and 2.)

'Generate list of all possible relative final positions for the cheat moves
var cListR() As Integer
var cListC() As Integer
for a as Integer = 0 to nCheatMax 'number of steps in one direction
  for b as Integer = 1 to nCheatMax 'number of steps in orthogonal direction
    if a=1 and b=1 then Continue 'skip the single diagonal jump; it accomplishes nothing
    if a+b>1 and a+b<=nCheatMax then 'move is in the range of 2..nCheatMax
      cListR.Add a
      cListC.Add b
      cListR.Add -a
      cListC.Add -b
      cListR.Add b
      cListC.Add -a
      cListR.Add -b
      cListC.Add a
    end if
  next b
next a

This generates all final offsets without duplicates. For part 2 there are 832 of them.

Then for each point on the path, try each of the offsets, and calculate the running distance difference for each one.

'For each position on the path, try each cheat jump
var chtTotal As Integer = 0
var testPt As new Point2D(0,0,gRows,gCols)
var sPt As new Point2D(0,0,gRows,gCols)
for pPos as Integer = 0 to pathListR.LastIndex 'loop for path position
  sPt.SetRC(pathListR(pPos),pathListC(pPos))
  var baseScore As Integer = score(pathListR(pPos),pathListC(pPos))
  for ic as Integer = 0 to cListR.LastIndex 'loop for cheat offfset
    testPt.SetRC(pathListR(pPos)+cListR(ic),pathListC(pPos)+cListC(ic))
    if testPt.iGridDataN(grid,0,0,-1) = path then
      var jmpSize As Integer = abs(cListR(ic)) + abs(cListC(ic))
      var chSavings As Integer = score(testPt.R,testPt.C) - baseScore - jmpSize
      if chSavings > 99 then 'Saves 100 picosecs or more, so add to the count
        chtTotal = chtTotal+1
      end if
    end if
  next ic
next pPos

Again, I used my increasingly useful Point2D class which handles bounds checking automatically, so that I don’t have to worry whether the jump offset puts the test position out of bounds.

Part 2 runs in 2.5 seconds on my 2015 MacBook.

I finally finished Day 16, and think I can finally handle the same type of puzzle from last year that flummoxed me.

Day 16 spoiler

This is a “find the best path” puzzle, and luckily I have a very flexible A* implementation based on a Class Interface. Normally I’d run this using my ObjectGrid class, but that was proving inadequate since I had to consider direction too in the cost.

So I created a new class that works around a multi-dimensional string array, or a String Grid, that implements the required interface. It stores the Grid as a property and returns cost based on the direction of the given parent. After working out a few bugs in my implementation, that worked great.

(Note that the cost of moving straight is 1, while a turn is 1001, not 1000.)

Part 2 presented a challenge because there is more than one “best path”. I understand there are other algorithms I could have implemented, but I did it locally.

I took my solution from Part 1, then worked off of that trail. For each square that had more than 2 neighbors, I blocked off the next square in the trail, then attempted to find a best path. If I could, I recorded the squares in a set, then called the same function recursively on the new trail. Finally I reset the next square to “.” and moved on.

(I recorded my best score in a shared property so I could short-circuit my path finder as soon as it started going down an invalid path.)

This solution (which I’m sure I can still optimize) takes about 7s.

I started to work on Day 16 last night. It’s basically the same solution as for Day 17 last year, but I still have to refresh my memory on the details. I think I still have a bunch of unnecessary code in there that was left over from several unsuccessful trials before I finally figured it out.

Meanwhile, I got a couple of other puzzles completed or partially completed.

Day 24 Spoiler

The set of logic operations in the puzzle input data is an implementation of a 45 bit wide binary adder, consisting of 45 one bit adder stages, all identical except for stage 0. Each adder stage (except stage 0) has 3 inputs (Xnn, Ynn and Carry_In), and 2 outputs (Znn and Carry_Out), and consists of 5 logic “gates” (2 AND, 2 XOR and 1 OR). Stage 0 has no Carry_In or the related Carry_In logic, so it has only two gates (1 AND and 1 XOR). Carry_Out from one stage is connected to Carry_In in the subsequent stage.

'  One bit Full Adder Logic
'    
'  CarIn o----------------------+----------|
'                               |          |----
'    Xnn o-+------|             |          |XORb|---------------o Znn out
'          |      |----   id1   |          |----
'          |      |XORa|------------+------|
'          |      |----         |   |
'    Ynn o-----+--|             |   |
'          |   |                |   +------|
'          |   |                |          |----   id3  
'          |   |                |          |ANDb|--------|
'          |   |                |          |----         |----
'          |   +--|             +----------|             | OR |-o CarOut
'          |      |----   id2                            |----
'          |      |ANDa|---------------------------------|
'          |      |---- 
'          +------|
'
'    Duplicate AND and XOR are distinquished by suffix a and b
'

My code organized the set of logic operations into a dictionary that was keyed to all wire ID’s and I/O ID’s, making it easy to pull all references to a given wire ID or I/O.

Then, the program went through each stage and traced the wiring, starting from the X & Y inputs and through the logic, comparing it to the correct logic, in order to locate any mis-wired outputs.

Stage 0 is trivial to analyze. With just two outputs, the one and only way to mis-wire them is to swap them. That is easily detected by looking at where output Z00 is connected.

The rest of the stages (1…44), being identical, were analyzed in a loop. The code is not very compact, but I couldn’t think of a more elegant way to do it. As it is, it gets the job done fairly quickly.

A final note: I made the assumption that swapped outputs would not be across different stages, but would be constrained to within a single stage. This reduced the amount of work considerably. Part of my reasoning was that cross-connecting between stages could result in a loop situation that would make results for part 1 of the puzzle unpredictable, and without that assumption, it would have made the analysis much more difficult. As it turned out the assumption was correct.

Wow Kim and Robert… you guys had an imrpessive Advent of Code run this year… both of you made it all the way to the last puzzle.

Hopefully I will get some time during the year to go and revisit some of the puzzles I got stuck with.

Anyhow… well done!

1 Like

I’m still working on them. Now that the holiday chaos is over, I have more spare time again. I finished a couple more in the past few days.

I have 5 more part 2s to do (including day 24) that have me stumped. Not sure I’ll get any further anytime soon.

I have three puzzles where I haven’t even done part 1 yet. I’m going to try to get them done so that I can find out what I’m in for with part 2.