Anisse

(replying to Anisse)

So, I'm not off to a good start. I took (what felt like) forever for part1, and then 4 times longer for part 2.

They were two traps in part 2, and I fell into both. The first is explained in the example (easy), the second one needs to be read between the lines, which took me a long time.

And this should be an easy day πŸ˜…

Anisse

(replying to Anisse)

Nothing special today, I almost solved both without using the example as a test, but a copy-paste error thwarted it.

I'm still quite slow, and I am starting to question my goal of using iterators all the way. Especially during parsing, they provide too late feedback, and I need to reduce the feedback loop.

Anisse

(replying to Anisse)

I'm still slow, but did not feel this one was too difficult. I did not need to use the sample test, so that's progress. I'm now only using it after a failure which is not obvious to debug.
After a few years of advent of code, I guess I'm getting used to grid puzzles and iterating over adjacent cells. Since I knew it was a grid, I also skipped trying to do it with an iterator in a single pass, and just went for a two dimensional Vec.

I took my own advice of attempting to reduce the feedback loop, so I used a second terminal that basically runs the program in a loop every 2s. So I can immediately see the results of adding a debug line, etc.

Anisse

(replying to Anisse)

Today was my fastest day yet, and it seems I'm not the only one.

Again, I only worked on the actual input, and used the fact that it has a fixed number of winning cards instead of parsing the separator (added it later after solving). I also abused the fact that every card number is < 100.

Again, the second terminal that runs the program in a loop helped a lot. Just gotta be careful when implementing infinite loops :-)

Anisse

(replying to Anisse)

Wasted a lot of time with parsing, but this is kind of a self-inflicted would with my iterator-based template (which I trashed today) :-)

I immediately saw that part 2 was suspiciously complex, and β€”eyeballing the inputβ€” decided to write a bruteforce solution based on my part 1. I let it run in release mode with my usual desktop notifier on completion, and then went to write the "actual" solution.

About 3 minutes in, I was barely designing the solution, and the bruteforce comes out with a result. Incredulous, I try it, and it was a correct answer ! Thanks to this I made it for the first time in the top 1000.

I then continued iterating on the "proper" solution, but it was even harder that I had suspected. I still think it could be simpler with a correct approach, and I might try to find the time later to study other solutions.

Anisse

(replying to Anisse)

Day 6 is probably the best day to get warmed up on Advent of Code 2023. Give it a go !

Anisse

(replying to Anisse)

I overslept this morning, and it had to be on an easy day :-( (hard days are more spread out so a few minutes late does not usually matter for private leaderboards)

Otherwise, I solved it and then decided to do the "math" solution. It was fun !

Anisse

(replying to Anisse)

I did not read the problem properly, and tried to solve something much more complex than the actual problem. After that it was not that complicated, even part 2.

Anisse

(replying to Anisse)

Today was alright. I took the straightforward solution of using an LCM for part 2. Took me a bit long to do because I didn't have one in my toolset, but for the next year I'll be ready with both gcd and lcm :-)

And then someone told me it's not a generic solution but it depends on the input design. And it's true. Looking at the input design, it's incredibly well crafted.
A colleague converted their input to dot and plotted it. Here is a subset showing exactly one part 2 cycle.

Notice how whatever the instructions, any path from A or Z will merge at the next node with the two same instructions. And the whole cycle L/R design is the same except the ending.

Subset of a part 2 input showing a cycle design.

Anisse

(replying to Anisse)

This was supposed to be an easy day, and it was if I look at my solutions once the debugging code is removed. But I stumbled multiple times:
* I was very happy to be able to use my ints parsing helper which I wrote the previous day. Alas, I found ~1h in that it lacked testing and did not support negative numbers, crucial for today's input.
* I chose the simple path (which uses more memory and does unnecessary work), but forgot that the line itself is part of the history.
* It took me a while to realize it was a simple sum() for part1, I had some complex folding that was unnecessary.

I'll do better tomorrow !

Anisse

(replying to Anisse)

For part 2 I took a wrong approach of flood fill on one side along the path of the pipe. Problem: you don't know which side is inside, so you have to hardcode one or the other. Plus my solution was buggy and complex. (I now have an idea on how to fix it, but that ship has sailed).

I scraped it and decided to use the "point in polygon" ray casting in one direction algorithm, which is well documented. Only issue is how to adapt the ray casting in one direction to the grid. It can be done with a bit of math to track when the intersection actually happen.

Anisse

(replying to Anisse)

I fell into the trap of expanding the map for part 1, and reading part 2, I quickly understood it was the wrong approach. Part 2 was simpler and faster to write 🀷

I played a bit with Rust iterators, and I've had widely different timings depending on the approach.
I thought doing a single pass init to find the expanded row, columns and coordinates would be a good idea, but it ended up being four times slower overall. I haven't investigated why, but it's probably cache shenanigans.

Same for my final solution, finding the galaxies coordinates, and then iterating over them is two times slower if I fused the two steps instead of collecting into an intermediate Vec. I did not investigate why either.

Anisse

(replying to Anisse)

As expected, the difficulty is ramping up. After adding many tests, my recursive part 1 solution finally passed.

I launched my this solution on part 2 as a background bruteforce, and kept looking for new conditions to reduce the search space.
Luckily, I did not attempt to generate all the arrangements, so that was not an issue; but it was still excruciatingly slow, with no completion in sight.
After a while, it hit me: when in doubt, memoize. This helped me pass part 2 in a reasonable time.

But the problem stayed on my mind all day, and I had to rewrite-it in the evening, cutting the solution length in half and making it much more readable by doing only necessary things.

side note: as I attempted to simplify the memoization code, I realized you cannot use the HashMap Entry API to do recursive memoization, because of the borrow checker. So my initial approach of doing a get, then an insert was the correct one by accident :-)

Anisse

(replying to Anisse)

As expected with this year's tik-tok difficulty spikes, this one was a bit easier.

The goal is to find a mirror line, and with proper accounting it can be done.

Small tip: using range iterators with zip() helped me a lot here.

Careful though: in you need to use rev() to iterate in reverse range order (if start > end, the iterator is empty); this took me a few minutes to realize my mistake.

Anisse

(replying to Anisse)

Despite the tip in the part 1 ("one of four directions"), I decided to not do any modification of the map and count the load in-place.

So I basically had to re-do part1 in part 2, and then make it work in all 3 other directions.

Then part 2 looked a lot like the cycle detection from advent 2022 day 17, so I decided to reuse my "tortoise and hare" cycle detection implementation from last year. Except it was not necessary, because the problem is simpler: there are no hidden dependencies between the steps, so a simple set or map can suffice.

But I also tripped because I initially detected the cycle based on the result (total load) instead of the full map: it does not work because the result *not* a strong hash of the map.

In the end, I replaced the result with the full map (it's copied many times), fixed the math a bit, and it worked πŸŽ‰ .

Later I looked at hyper-neutrino's solution, and it's incredibly simple:
youtube.com/watch?v=WCVOBKUNc3 ; yes, it might be less efficient than what I wrote, but I really liked the approach. If I have the time I might try to re-implement it in Rust.

Anisse

(replying to Anisse)

This one was a bit easier, I had to re-read multiple times the part 1 because I thought it couldn't be that simple. But it was. Except the part where I didn't read properly the bold message "Ignore newline characters".

Part 2 was slightly challenging, with rust lacking an order preserving hashmap (linked_hash_map on crates.io should do the job, but I usually don't add crates I haven't vetted before).

Anisse

(replying to Anisse)

Probably one of the days I enjoyed the most. Maybe because I decided yesterday to miss the release time and slept a bit more this morning :-)

Overall this takes a lots of tricks we learned over the years through AoC about grid problems, and mixes them a bit.

My solution is relatively short and could maybe be reduced even further, but I'm currently happy with it, and it's what matters πŸ™‚

Anisse

(replying to Anisse)

The premise seemed simple, it's a variation on a problem we've seen before. Yet it took me quite a while to find a working solution, and even then, it takes very long (4-5min in release mode for part 1, half a second for part 2), so I have a hidden bug in my implementation.
But I guess that will have to do for now! Onward !

(and ) learning of the day: you can compare tuples, and they will be compared element by element in order.

Anisse

(replying to Anisse)

I lost a lot of time today, on multiple issues, the biggest being that I forgot how to parse... an int.

Since I usually look at the hashtag after solving, on day 10 I stumbled on @xavdid's solution. So for today, I immediately recognized today's problem as one where the Shoelace formula and Pick's theorem could be applied, so I tried to implement that. But I misunderstood that on a grid, every point (not just vertices) should be counted in the boundary integer points. It took me a while to fix.
But then it did not work.
So I decided to implement a flood fill, in a map of +1 size on each edge. But it gave the same result as the shoelace + pick formula.

So It had to be one of two things: either the problem was wrong on my given input, or I had been given the wrong input. So I kept looking. And while browsing the input, I found the issue: some number of moves where on two digits, but I only parsed one (!). I'm not sure why I did that, I guess I wanted to be clever with bytes manipulation

Once that was fixed, part 2 was smooth sailing with just a bit of parsing since I already had a shoelace + pick implementation.

Anisse

(replying to Anisse)

Solved part 1 today without the example, it was mostly parsing code. Not having eval() does not help, but at least the code is fast.

For part 2, I quickly identified the family of problem, but my solution was a bit convoluted. I should have taken more time to think it through.

I also side-stepped most off-by-one issues, except the most obvious one, where the range started at 1 instead of 0.

Anisse

(replying to Anisse)

I lost a lot of time today on understanding the problem statement (~15min of reading), and then parsing. I wanted to put Rust &str everywhere to reduce copies/allocations, as it worked well for day 19; but the way I structured the data made this impossible, so I quickly resorted back to String. In retrospect, of course it does not matter for this problem; and there at most two bytes of information per module name, so either solution is overkill anyway.

In part 1, I saw the problem statement with explicit examples showing that events should be handled in a queue (breadth-first), and I thought: "No need, I can do this recursively (depth-first), there won't be side effects". So I went on with a recursive solution. After 10 min of debug (or more?) it was obvious that, no, there are indeed side effects, and a queue is needed. I re-wrote it properly, it even simplified some aspects (accounting for the button press for example). Once the tests passed,

Then part 2 came, and the problem looked simple. I thought, the only way this works is that there is some kind of cycle; probably multiple cycles with LCM so we can't bruteforce it (I launched a bruteforce version anyway).
So I started modeling how this could work, and explored a bit the data. But it took me a long time to arrive to a working result, because of two blocking issues:
- the output pulse (of each sub-cycle) is hidden during a button press update cycle; if you let it converge, it will go back to a state with no output, and no cycling can be detected.
- I was convinced that for some reason looking inside an update cycle wouldn't work because it would break the congruence with the button presses. But the problem input is designed so that it works and that the output is at the "same time" (also a side effect of the breadth-first exploration) for all four cycles; and each cycles does stabilize after its period.

It was definitively the wrong day to lose time on part 1; I would have had more time to analyze the input. I'll probably try to graph them more next time.

Anisse

(replying to Anisse)

This one was quite long for me. Part 1 is designed to give you hints for part 1, like showing you the properties of the input.

Part 2 took me more than 10 tries. I quickly understood that shortcuts could be taken based on the input and problem constraints, but even then, it took quite a while. I had to write custom tests, since the examples aren't really useful for my input-tailored solution.

Once that was done, I could use part1 as a reference, and iron-out the last bugs.

Probably the hardest day (yet) for me, and I'm not proud of the code, which didn't go through the usual cleanup rounds.

Anisse

(replying to Anisse)

A bit lighter day, though it did not feel easier at first. In fact part1 was clearly identified as two parts:
- first make the magic blocks fall; includes making 3D collisions work and "editing" or "compressing" the input. It has some examples, which could be used to make test cases.
- then identify which blocks, when removed, do *not* generate any more "compression" or falling blocks. This second half of part 1 could reuse the first half, but I chose not to, thinking it would help with part 2: the complexity seemed approachable, but I could imagine other problems where it would not scale. So I built two maps of which blocks support which and the opposite one (which is supported by which), and then used that to solve the problem. Once the sample passed, so did the input.

For part 2, I was right, and the two reverse maps were needed to explore the set of possibilities, with a recursive solutions with a set of fallen blocks.

My solution has many inefficiencies, and clocks at around 2s in release mode, but that will have to do for today πŸ™‚

Anisse

(replying to Anisse)

This one went smoothly. I did the simple recursive depth-first exploration for part 1: I figured a full exploration would be doable since the slopes are heavily reducing the search space.

It succeeded in the first try with no test.

Then part 2 relaxed a bit the conditions, making the search space much bigger. As usual, I went for a bruteforce-first "just-in-case", and then kept looking.
The problem looked like it would need dynamic programing. I was debugging such an approach while the bruteforce returned something. Tried it, and it worked !

I continue debugging the DP approach, and even found the optimal result for the given example. But I now knew the real answer, and it just did not work on input.
In reality, this is a Longest Path in Cyclic Graph problem, one that is NP-hard. It's possible to reduce a bit the search space with edge transitive reduction (remove cells that are only adjacent to two cells), but I opted to simply get a bigger stack (and then, this is only needed in debug mode, not release mode).

My only regret is that I used a set of coordinates for the seen nodes instead of two-dimensional boolean array (which I usually reach for), wasting ~15min in my bruteforce approach. Final solution takes about 4min, with no edge reduction. I need a faster computer πŸ™‚ (usually one doesn't for AoC 😞)

I ranked 698 for part 1 and 185 for part 2! Looks like I broke two records on a single day !

Anisse

(replying to Anisse)

I could not resist, so for day 23, I wrote an edge-reduction (+trivial dead-end elimination) solution, and it takes ~300ms in release mode.

I still have no regrets because it took me longer to write than the time to write and run my first naive bruteforce solution.

On the plus side: I generated a graphviz of the reduced input which helped understand why it was needed:

Graph of nodes that have coordinates inside of them, with edges showing high weights (often > 100).

Anisse

(replying to Anisse)

I solved part 1 in an average time (for the later days); the real problem was part 2. I did not know how to approach the solution for a long time; so this was definitely the hardest problem this year.

After failing to solve part 2 in Wolfram Alpha (reaching the limit of characters), I ended up writing a bruteforce solution which found the solution from the example pretty quickly. But as I had guessed, it did not work for the much bigger input. I did not let it run for more than 1 hour.

So the next day, I caved and started to learn how to use z3 with the rust bindings crate. It took some wrestling (no examples, buggy build on fedora fixed in git but with no new crate), but I managed to do it, and it finds a solution in a few seconds.
I now know it can be solved in ~constant time, but I was too tired to re-write (again) the equations to solve them, or to learn about and implement Gaussian Elimination.

Anisse

(replying to Anisse)

As I saw the problem today, I was reminded of how useful was visualization on day 20.

So I immediately tried to learn about the dot language (that was fast), and whipped up some awk to convert the input. The three connection edges were quickly apparent, so I removed them manually, made the other edges bi-directional and counted the nodes on each part of the graph.

I know there is a simple graph-theory solution with flows, but that will be enough for me for this year's AoC !

Graphic visualization of my input for day 25 of AoC 2023.

Anisse

(replying to Anisse)

It's the second year in a row that I'm able to complete AoC. And I'll be honest: this was though. The break-neck pace of doing one full solve per day requires that one either has a lot of free time, or has previously spent a lot of free time getting better in order to solve the puzzles faster. I do it because it's fun, and it still was; but I'm aware that it's walking over the not-fun line as well.

I was able to do it, but it was not cost-free. I think I might go for a lighter pace (or skip) next year.

On a positive note, I still learned a lot this year, and that's one of my personal goals πŸ™‚

Anisse

(replying to Anisse)

I summarized everything I learned from completing this year's Advent of Code in Rust: anisse.astier.eu/aoc-2023-less

It is mostly spoiler-free and does not talk about any specific problem.