Advent of Code 2023

Day 3 spoiler

For the second part, I’ve used Rect.Intersects to check the collisions with the other number bounds :upside_down_face:

rect-intersects

3 Likes

I am building myself a 2D class in Swift this year. Helped me last year (in RB) already, too.

Spoiler

I first scan each line, detecting numbers with Regex, then create a “line” in my 2D model from the positions of the numbers’ digits, then added an “outline” function to my 2D class in which I return the coords of every point around a given point or around a set of points (using a Set in which I remember which points I already added, in order to prevent returning the same point multiple times). With that, I’d then go over the outline points, looking for a “*”. If I find one, I add the number into a dict for that *'s coordinate. In the end, I go over all found “*” and check their count - if it’s two, I multiply the two numbers, and add them to the result. So, the key function here was to use a 2D model and add a line and an outline function. Others do the same, without the help of a separate 2D class, but I found this to be useful in a class for later challenges.

BTW, I also ran into a tricky and hard-to-understand behavior with (im)mutable arrays used as dictionary values, see https://stackoverflow.com/q/77596160/43615

Does that handle the case that there may be 3 numbers adjacent to a ‘*’? Because those do not count!

Just a reminder: Better leaderboard stats with Chrome and FireFox:

My original plan had been to take into account 3 or more part numbers, and add each new one to the dictionary entry for the corresponding gear symbol, then not count them until all input lines had been processed. Then, only entries with 2 part numbers would be considered. However, I suspected that the data had probably been constructed to prevent that from happening (or there might have been a note to indicate the possibility), so I decided to try it by just counting the gear ratio as soon as a second part number was found, and that gave me the correct answer. No need to make the program more complicated than necessary.

1 Like

My Advent of Code project contains a script that will start a new day for me. Included in that are functions that will fetch and store the puzzle input for the day in a file, in case that’s useful to anyone else.

Edit: It will work on a Mac and maybe Linux, but would have to be adapted for Windows.

Day 5, part 2… Wow.

Wow wow wow. How is this a day 5 puzzle?!?

Day 5 part 1 was easy enough. It was just a slog parsing the input data. For part 2 I’ll probably have to do a complete rewrite…tomorrow. I’m off to bed now.

Hahahahaha… Kem… I had the same reaction. Almost thought at one point I’ll have to dive into GPU parallel processing to get the answer with brute force.

These puzzles definitely challenges you to think creatively about alternative solutions.

Would love to hear how others solved part 2 of day 5…

Day 5 Spoiler

What I eventually did was to reverse-map from location back to seed.

So basically start at location 1, map it all the way back to the seed… and then check if the seed number exists. If not… location = location + 1 and next iteration.

Day 5 spoiler

I’m surprised that worked, and it may not have for other data sets. Maybe this isn’t true for anyone’s data, but it’s possible that the final answer is not covered by any of the Location values, so it could be a value that is passed down from one of the previous converters.

I’m also curious how long this takes to run since (it sounds like it’s) a brute-force solution.

Anyway, if it worked, it worked.

I used my Range class in both parts. In part 1, I cycled through the converters looking for a Range that covered a value, converted it, then went onto the next one. In fact, thinking about it, I probably overdid it so I’ll have to go back and refactor. I didn’t need a Seed class, for example.

I found part 2 hard, even if I had a rough idea from the start on how to handle it. I again used Range to represent each set of seeds, then cycled through each consecutive converter to get the destination ranges until I worked down to Location.

The trick was in the overlap. For example, say a seed range was 4-6 and the first converter covered 1-10 and converted that to 22-31. I would have to reduce the destination range by changing its min and max to end up with 25-27. That would get passed down the next converter.

But what if the first converter covered 5-10 → 25-30 instead? Now I need to end up with two ranges, the part that’s covered (5-6 → 26-27) and the part that’s not (4-4).

More, what if 4 was covered by another range within the same converter? I had to try the 4-4 range again to see if it too could be converted. Only when a single range returned a single range could I collect all the results and move onto the next converter.

If that had been the last step, I would have ended up with two ranges, 4-4 and 26-27, and my answer would have been 4.

Day 5 spoiler

I’ll have to put some thought into my approach… not sure I fully understand why it won’t work with all datasets.

That being said, definitely not the kind of code one would use in production. It took roughly about 15 seconds to run, which would be a problem with different datasets or when the location is a very high number.

So it worked, but just barely :sweat_smile: Will definitely revisit it again to refine it.

If I may ask… how long did your code run to solve part 2 Kem? And would you mind making your code available? Would like to work through it and learn from it if you don’t mind.

This text will be hidden

In the IDE, about 10 ms. Compiled, 1.5 ms.

My code is already available, see the github link in an earlier post.

2 Likes

:muscle:

Thank you

Day 5 discussion

Let me see if I can simplify my thought. (And again, this may only be theory.)

Let’s say there is only one converter, Seeds → Location, and it has exactly one conversion range:

Seeds-to-Location: 10-15 → 20-25

If you were to give it a single value, let’s say 10, it would be converted to 20, 11 to 21, and so on. If you were to give the range 10-12, that would be converted to 20-22. In that case, 20 is the lowest value.

But say you gave it the seed range 8-12. There is no conversion for 8 or 9 so those remain as-is, and the resulting conversion is 8-9 & 20-22. In that case, the lowest Location is 8 even though the Location converter did not cover that value.

Day 5 discussion

Oh… I see now what you meant. I did cater for the “no conversion” scenarios (or at least… I thought so).

Here is the code snippet for the reverse conversion… FYI… the map is an integer array that stores the [source, destination, range, source, destination, range…] values. My caveman version of your range class.

function MapDestination(destination As UInt64, map() As UInt64)

Var source As UInt64
Var i As UInt64
Var foundRoute As Boolean

i = 0
foundRoute = false
while (i < map.Count) and not foundRoute
if (destination >= map(i + 1)) and (destination < (map(i + 1) + map(i + 2))) then
source = map(i) + (destination - map(i + 1))
foundRoute = true
end if
i = i + 3
wend

if not foundRoute then
source = destination
end if

return source

So I pass in the destination (instead of source) to the converter, and then the source is returned… basically everything just in reverse.

the “if not foundRoute then souce = destination” code at the end is where I catered for the use case you described above.

But the reverse mapping is not a practical solution anyways. Like you pointed out, it is basically a brute-force approach that will take a very long time with some datasets.

I worked through your code, and your range class and the use thereof is a great solution.

That is what I like about AoC… it is amazing how much you learn just by attempting to solve these puzzles.

2 Likes

And the techniques translate to day-to-day, production coding.

2 Likes

Day 6 was comparatively easy.

Day 6 spoilers

My first iteration solved part 2 in about 10s or so in the IDE. Two revisions later, it was about 60 µs.

The trick is, you need to find the first winning hold and the last one, then last - first + 1 to find the number of winners. With that in mind, you don’t have to check every possibility. My second revision was to loop forwards until I found the first winner, then backwards until I found the last one. That cut it down to about 3s.

Then I implemented a binary search to find the first one, and another to find the last. The end result was, out of almost 59M possibilities, I only had to check about 50.

It turns out that, compiled, even my original day 6 solution took around 200 ms. Oh well.