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.