sharick.xyz Home Blog Projects Directory Misc Contact

Advent of Code 2023, Week 1

Tags: Advent of Code

Advent of Code

Advent of Code is a yearly Advent calendar of programming puzzles that's run since 2015, with 25 two-part problems each year, each part rewarding one "star". For 2023, after completing every previous year, I'm making a weekly blog post about that week's problems, containing my thoughts on them and how I solved them; these, of course, will contain spoilers. For the code, see:
2023 ยท personal library.

Day 1: Trebuchet?! (puzzle, code)

Solve Times: 04:55 / #2015, 19:27 / #1668

This problem is way harder than I expected for a Day 1, especially its part 2; the stats bear that out, as does the reception everywhere I've seen. Part 1 asks to find the earliest and latest digits in each line, turn them into a 2-digit number, and sum them; part 2 is the same, but the English names (for example, "one", "five", or "eight") count as digits as well.

My approach was the "simple" approach to just use Python's find/rfind to find the lowest and highest index of a string-digit, and comparing it to the lowest/highest index of a real digit. This allowed me to avoid common pitfalls people hit with other approaches, but it's not very clean, and I was slowed down by a lack of organization leading to avoidable mistakes. Overall I'm not super happy with this solution, but it works and I can't really do much about it anymore.

Day 2: Cube Conundrum (puzzle, code)

Solve Times: 07:18 / #567, 08:48 / #332

This puzzle, unlike the previous one, is pretty on par with the expected difficulty level. In this puzzle, there's a series of "games" (one per line), each of which contains multiple "rounds" which involve pulling out some number of red, green, and/or blue cubes from a bag and then putting them all back. In the first part, the goal is to figure out which many games are possible with 12 red, 13 green, and 14 blue cubes in the bag, and sum their IDs; in the second, it's to find the smallest number of cubes of each color needed to make every game possible.

This puzzle went reasonably well, although I missed out on noticing the optimization where the separation between "rounds" is irrelevant, because cubes are always returned anyways. I turned each line into a list of dicts, one per round, and used them to check which rounds were valid; I realized only in time for part 2 that I could just use a Counter (one of my favorite Python builtins) to handle missing data, which meant my part 1 logic was a bit overcomplicated. For the latter part, I calculated the value of each game with a one-liner of sorts: max(rnd["red"] for rnd in g) * max(rnd["blue"] for rnd in g) * max(rnd["green"] for rnd in g), which I easily summed across the games.

Day 3: Gear Ratios (puzzle, code)

Solve Times: 18:59 / #1314, 23:57 / #848

This was a trickier problem again, with a lot of (literal) edge cases to account for and a surprisingly early appearance for 2D grids. It presents a grid of periods ., digits 0-9, and symbols like *, $, and #. In the first part, the numbers adjacent to said symbols are "part numbers", and the goal is to find the sum of all part numbers; in the second, the * symbols, if they're adjacent to exactly two part numbers, are "gears", with a "gear ratio" equal to the product of those numbers, and the goal is to find the sum of all gear ratios.

For this problem, I iterated through the 2D grid until finding a digit, then found the entire number by moving in the +x direction until hitting a non-digit (or the end of the row). With that, I looked at every coordinate bordering the number to see if any were symbols, and if so, added that to the sum, using a hideous one-liner condition: if (y > 0 and any(get(inp, y - 1, x1) in symbols for x1 in range(x - l, x + 2))) or (y < len(inp) - 1 and any(get(inp, y + 1, x1) in symbols for x1 in range(x - l, x + 2))) or get(inp, y, x - l) in symbols or get(inp, y, x + 1) in symbols:. It contains four parts: the row above the number, the row below the number, and then the two positions directly left and right of the number. Despite its ugliness and unmaintainability, it worked, but for part 2 I took a different approach. Here, I instead collected the list of positions around the number in a less ugly way, and looked in each of them for gear symbols *; I used a Counter to track the # of part numbers bordering each gear, and a defaultdict(list) for the list of bordering numbers. Once I finished that, I iterated through to find all the valid gears where the Counter's value was 2, and then summed them up.

Day 4: Scratchcards (puzzle, code)

Solve Times: 03:49 / #305, 24:45 / #3230

This puzzle is a game of scratchcards which have some amount of winning numbers, followed by a larger amount of "numbers you have". In part 1, each scratchcard awards points based on the number of winning numbers in the "numbers you have", with 1 point for 1 match, 2 for 2, 4 for 3, etc., or in summary 2^(n - 1) for at least one winning number. In part 2, instead, the scratchcards give you new copies of future scratchcards: one winning number gives one copy to the first scratchcard following, two gives one copy to the next two, etc., and the goal is to find the total number of scratchcards at the end.

Part 1 was fairly simple to tackle, since it just requires looping through the "numbers you have" to find how many are winners, getting the points from that, and summing them. Part 2 is also simple in concept: initialize a list (or dict) to 1 copy of each card number at first, increase them based on the results of previous cards, and use the list to account for the number of copies of the current card there is when doing so. However, on part 2, I kept making avoidable mistakes due to misreading the problem text, such as thinking the number of cards ahead to copy was still exponential (like part 1's points) instead of linear. I eventually realized my mistake, but it did drag me down significantly.