aoc

My solutions for the Advent of Code
git clone https://git.tronto.net/aoc
Download | Log | Files | Refs | README

README.md (10063B)


      1 # Advent of Code 2025
      2 
      3 Run with Python. Input is read from the file given as argument, or
      4 from standard input if no argument is provided.
      5 
      6 Example
      7 
      8 `python 01/a.py 01/input.txt`
      9 
     10 ## Personal times
     11 
     12 ```
     13 Day   -Part 1-   -Part 2-
     14  12   00:58:32   00:58:36
     15  11   00:14:22   00:21:19
     16  10   00:27:43   11:51:51
     17   9   00:05:05   02:11:41
     18   8   00:29:14   00:33:02
     19   7   00:05:27   00:20:40
     20   6   00:13:38   01:49:24
     21   5   00:04:39   00:22:19
     22   4   00:25:36   00:27:49
     23   3   00:05:02   00:16:09
     24   2   00:10:36   00:11:55
     25   1   00:06:20   00:50:41
     26 ```
     27 
     28 ## Daily comments (spoilers!)
     29 
     30 ### Day 1: Secret Entrance
     31 
     32 Part 1 was easy. Well, I misread the statement and got it wrong twice,
     33 but that's on me.
     34 
     35 Then I kept getting part 2 wrong, and I have no idea what I was doing wrong.
     36 Sure, at first I made some mistakes related to landing on zero at the
     37 end of a movement. But for at least 6-7 times I found a mistake, fixed,
     38 my code would solve the example case correctly but my answer on the real
     39 input was wrong. At some point I started rewriting the code in alternative
     40 ways hoping to get a different answer, and one of those versions worked.
     41 Still no idea why.
     42 
     43 So in the end it took me more than 40 minutes to solve part 2. Great start!
     44 
     45 ### Day 1: Secret Entrance
     46 
     47 I misread the statement for part 1 and I accidentally solved part 2 first.
     48 I re-read the problem description, fixed my code, submitted it, and then
     49 I just had to press `Ctrl+Z` (actually `u` on vi...) enough times to solve
     50 part 2.
     51 
     52 ### Day 3: Lobby
     53 
     54 My solution for part 1 is ad-hoc and not very good. I should have thought
     55 about it a bit longer, the general solution isn't much harder to find.
     56 
     57 For part 2 I used recursion with memorization (using Python's
     58 `functools.cache`), which is fast enough. But later Chiara pointed
     59 out to me that actually the solution is quite trivially greedy; I
     60 implemented the greedy version in `b-alt.py`.
     61 
     62 ### Day 4: Printing Department
     63 
     64 The first "map" problem of the year! This one was easy, but I
     65 made a lot of mistakes in part 1. I decided to use the trick that
     66 [Jared](https://guissmo.com) suggested a couple of years ago: extend the
     67 map by 1 cell in all directions so you don't have to deal with indices
     68 out of bounds.
     69 
     70 For part 2 I decided to quickly code the dumb "repeat part 1 until no
     71 rolls are removed" strategy and it worked.
     72 
     73 ### Day 5: Cafeteria
     74 
     75 Part 2 required a little bit of thinking to handle overlaps correctly
     76 - at first I wrote a solution that did not handle overlaps, then one
     77 that can only handle single overlaps, and finally one that works in
     78 every case. My final solution is quite straightforward.
     79 
     80 ### Day 6: Trash Compactor
     81 
     82 Part 1 was easy, but I struggled with part 2.
     83 
     84 I had to solve this while travelling, which did not help, but that was
     85 not the main issue. The problem was that for whatever reason my script
     86 did not copy the whitespaces correctly from the sample input in the web
     87 page, so I was left wondering how the heck am I supposed to align the
     88 numbers. After more than an hour and after changing train, I figured out
     89 the error and I was able to solve this elementary school problem. Much
     90 smart, very accomplishment.
     91 
     92 This year so far the only problems that took me more than 30 minutes
     93 are this and the first one, not exactly the hardest problems imaginable.
     94 
     95 ### Day 7: Laboratories
     96 
     97 This was quite fun! For part 1, I iterate over the rows of the diagram
     98 keeping a list of the position currently occupied by a tachyon. I use a
     99 Python set to avoid duplicates. Part 2 is very similar, but the set is
    100 changed to a map where the keys are the positions and the values are the
    101 number of multiverses where a tachyon is in that position.  To update
    102 this value for the current row, I sum the values of all tachyons that
    103 end there from the previous row (that can be one or two tachyons).
    104 
    105 I added a second solution for part 2 that does not use a map, but only
    106 lists. This could be seen as a dynamic programming problem where the
    107 iterative implementation is more intuitive than the recursive one.
    108 
    109 ### Day 8: Playground
    110 
    111 This one required some optimization effort for part 2... unless one is
    112 already familiar with [the algorithm
    113 described](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm)
    114 in the problem statement, and I was from back in the days of competitive
    115 programming. In the end it was mostly a matter of figuring out the correct
    116 [data structure to represent the groups of joint
    117 boxes](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) (or
    118 a matter of remembering how it is implemented, if one already knows it).
    119 
    120 ### Day 9: Movie Theater
    121 
    122 For me this was the first challenging problem (at least excluding those
    123 that were challenging because of mis-read the problem statement or
    124 because of sneaky bugs).
    125 
    126 Part 1 was easy as usual. For part 2 we go through every pair of
    127 corners and we check if the rectangle between them is admissible. A
    128 rectangle is admissible if none of the line segments that make up the
    129 figure "breaks" it. Breaking a rectangle means that either the lines is
    130 internal to it (and thus the points on one of the sides of the line are
    131 invalid but inside the rectangle) or that the line overlaps (at least
    132 in part) with one of the sides of the rectangle, and the exterior of
    133 the figure is in the rectangle. To know which side of the line is the
    134 interior and which is the exterior, I do some pre-processing to compute
    135 if our border winds clockwise or counter-clockwise (using the [cross
    136 product](https://en.wikipedia.org/wiki/Cross_product)).  It may be that
    137 every input has clockwise-winding border, but it was not much work to
    138 implement this pre-processing.
    139 
    140 My implementation of the function that checks if a line breaks the
    141 rectangle looks like a bunch of very smart Math, but don't think I am
    142 smart enough to write it on the first try: first I wrote it in 4 separate
    143 cases (line is vertical / horizontal, t is positive or negative) and
    144 then I merged the cases together removing the duplication. And I did
    145 not merge the cases together to look smart, I did it because it made
    146 debugging easier (did you think my code worked on the first try? ahah).
    147 
    148 This algorithm is O(n^3) and runs in about 6 seconds on my small laptop
    149 (or in about 4 seconds on my desktop). I was not sure O(n^3) was enough,
    150 and in fact I wasted a lot of time searching a quadratic or O(n^2log n)
    151 solution before I focused on formalizing a cubic one. The problem is that
    152 I did not have smaller inputs to try my code against, so I this solution
    153 was too slow I did not have any way to check that it was at least correct.
    154 
    155 EDIT (about 12h after solving part 2): apparently my algorithm is not
    156 entirely correct. In particular, it can fail by selecting a rectangle
    157 that is completely external to the figure and none of whose sides overlaps
    158 any of the lines.
    159 
    160 For example with this input:
    161 
    162 ```
    163 101,51
    164 101,0
    165 0,0
    166 0,2
    167 1,2
    168 1,1
    169 100,1
    170 100,50
    171 99,50
    172 99,51
    173 101,51
    174 ```
    175 
    176 The first version of my program return 4851 instead of 202. I added a
    177 check for this case in `b-fixed.py`, hopefully it works in 100% of the
    178 cases now.
    179 
    180 ### Day 10: Factory
    181 
    182 Phew, this was a hard one!
    183 
    184 Part 1 was quite easy, I just try all combinations of button presses
    185 until one works. It still took me relatively long to type it out.
    186 
    187 Part 2 was hard. Maybe not as hard to come up with as Day 9, but it
    188 took me much longer to thing through and type the complete solution.
    189 It is the first problem of this year's edition that I cannot fully solve
    190 before going to work (problems come out at 6AM in my time zone).
    191 
    192 So at first I tried to implement a recursive solution with memoization
    193 (using Python's
    194 [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)), but that was too slow. You can still check it out
    195 in `10/b-recursive-slow.py`.
    196 
    197 Then I thought about using linear algebra. I also had other ideas,
    198 but one particular sentence in the problem's statement strongly hints
    199 at that: *"You have to push each button an integer number of times;
    200 there's no such thing as "0.5 presses" (nor can you push a button a
    201 negative number of times)."*
    202 
    203 Like, come on. This sentence makes no sense at all in the context
    204 of this problem. It is only useful if you are already thinking about
    205 using linear algebra to solve this, but somehow along your reasoning
    206 you forgot that you are working with non-negative integers?
    207 
    208 Anyway, I briefly tried using [numpy's linear system
    209 solver](https://numpy.org/doc/stable/reference/generated/numpy.linalg.solve.html),
    210 but it would have been more complicated to figure out how to use this
    211 library in my case than to re-implement the necessary algorithms from
    212 scratch. And so I rolled up my sleeves and started coding... after all,
    213 it is not the first time I have to home-cook some linear algebra for an
    214 AoC problem (see `../2023/24/24b.c`).
    215 
    216 This time I left some comments in the code, so check out `10/b.py` if
    217 you want to know the details.
    218 
    219 ### Day 11: Reactor
    220 
    221 This is very easy, at least if you have ever worked with graphs. For
    222 part 1I have implemented a recursive function `np(v)` that counts the
    223 number of paths from a node `v` to `out`: it returns 1 if `v == out`,
    224 or the sum of `np(w)` for all neighbors `w` of `v` otherwise.
    225 
    226 For part 2 the function takes 2 extra parameters that denote whether
    227 or not we have passed through the two required intermediate nodes.
    228 
    229 The paths in part 1 are small enough that memoization is not required,
    230 but in part 2 we need to cache the intermediate results.
    231 
    232 ## Day 12: Christmas Tree Farm
    233 
    234 This problem is literally a prank, I did not like it. I feel bad for the
    235 people who actually try to solve it.
    236 
    237 The actual problem of trying to fit all the presents optimally is
    238 impossible. Maybe you can come up with an algorithm that works in theory,
    239 but it's the kind of thing that won't finish until the starvation of
    240 the last star in the galaxy or stuff like that.
    241 
    242 But you can try some simple heuristics, like: if I could chop the presents
    243 in 1x1 pieces, would they fit? Of course this condition is only necessary,
    244 and never sufficient... unless you are being pranked. Like in this case.