Rules: no spoilers.
The other rules are made up as we go along.
Share code by link to a forge, home page, pastebin (Eric Wastl has one here) or code section in a comment.
Rules: no spoilers.
The other rules are made up as we go along.
Share code by link to a forge, home page, pastebin (Eric Wastl has one here) or code section in a comment.
I am using dart to develop my dart chops. All code for all days here:
https://github.com/Fluxward/aoc2023
Nice, I’ll be reading :D
(Possibly have some dart coming up on a thing soon)
I’ll be honest: so far, Dart is pretty rubbish for this kind of exercise for the simple reason that their Strings aren’t just arrays of chars. There’s no native isDigit, for example. Otherwise, I’ve been using it with Flutter and have been happy with my experience.
I’m only posting code when I think I’ve done something interesting or if there’s a notable language feature to show off, but so far, no dice.
1 a,b:
as a professional software engineer, I know that googling regex syntax and getting it right will take longer than manually writing string comparisons, so… here’s all you need to see of my final solution.
int? check3(String line, int index) { if (line.length < index + 3) { return null; } String sub = line.substring(index, index + 3); return sub == "one" ? 1 : sub == "two" ? 2 : sub == "six" ? 6 : null; }
4 a
Not so bad today. I bit the bullet and tried to see if dart has tuples or similar. It does, by the name of “records”. Now instead of pretending I’m writing in C/C++, I can pretend I’m writing in python.
Anyway, a) is a pretty straightforward job-interview style question, except AoC doesn’t care about efficiency. Still, we all have our own versions of pride, so I did it with a set (Though whether or not dart’s native Set is tree or hash based is not known to me right now).
code
int matches(String line) { ({List wn, List n}) card = getNumbers(line); Set wn = Set.from(card.wn); return card.n.fold(0, (prev, e) => prev + (wn.contains(e) ? 1 : 0)); } void day4a(List lines) { print(lines.fold(0, (acc, line) { int m = matches(line); return acc + (m != 0 ? (1 << (m - 1)) : 0); })); }
4b
b) was a little harder, and definitely a possible trap for overthinking. I think the easiest way to think about solving this is as if it is a dynamic programming problem (though it kinda isn’t).
So the general approach is to formulate it like this:
T_n(total cards won by card n) = M_n(total matches for card n) + CW_n(total cards won by the copies that card n wins).
and
CW_n =
Caching T_n is the DP trick to making this performant (though once again, it does not need to be)
Anyway, the above approach is the top-down version of the DP; the bottom-up version is what I actually started with in my head. I gave up on that approach because I felt like the logic was too hard for me to figure out.
code:
void day4b(List lines) { List totalMatches = lines.map((e) => matches(e)).toList(); // total copies won, including copies won from copies. List cachedWins = List.filled(totalMatches.length, -1); int totalWins(int i) { // return cached result if it exists if (cachedWins[i] > 0) { return cachedWins[i]; } int count = totalMatches[i]; // count the copies won from the subsequent copied cards for (int j = 1; j <= totalMatches[i]; j++) { count += totalWins(i + j); } // cache the result cachedWins[i] = count; return count; } int totalCards = totalMatches.length; // count all the originals // count all the wins for (int i = 0; i < totalMatches.length; i++) { totalCards += totalWins(i); } print(totalCards); }
2 a,b:
This one is just a test of whether or not your language lets you split strings.
Only novel thing I did here was recognise that red, green and blue end in different letters, so no need to string match the whole world, just check the last letter of the colour.
3 a
I read this wrong initially and thought that you only needed to check adjacency on the same line. Whoops! Then I wrote a bad algorithm that finds numbers THEN searches for symbols. That alg isn’t inherently bad, except…
3 b
If I had chosen a symbol first approach, I would not have had as much pain as I did here. Also, I probably under and overthought this one. I went with my first idea, which was guaranteed to work.
The approach this time was:
A simpler approach would consider that you only have two numbers on the same line for the same gear if the character in the gear column is a non-digit; otherwise, if a number is adjacent to a gear, there is only one on that row. Union-find is completely overkill, but I like using it even when I don’t need to.
Anyway, upon reflecting on this, while the general approach is fine, I didn’t think too hard about the implementation and just ended up with globs of overly memoized spaghetti. I probably should check if Dart has a python-like tuple object or similar. Whatever. Behold!
void day3s() { List lines = [ for (String? line = stdin.readLineSync(); line != null; line = stdin.readLineSync()) line ]; List> digs = [for (int i = 0; i < lines.length; i++) Map()]; int? isDigitMem(int r, int c) { return digs[r].putIfAbsent(c, () => isDigit(lines[r][c])); } // first entry is parentc, second is size List>> uf = List.generate( lines.length, (i) => List.generate(lines[0].length, (j) => [j, 1, -1])); int find(int r, int c) { if (uf[r][c][0] != c) { uf[r][c][0] = find(r, uf[r][c][0]); return uf[r][c][0]; } return uf[r][c][0]; } void union(int r, int cp, int c) { cp = find(r, cp); c = find(r, c); if (c == cp) { return; } if (uf[r][cp][1] >= uf[r][c][1]) { uf[r][c][0] = cp; uf[r][cp][1] += uf[r][c][1]; } else { uf[r][cp][0] = c; uf[r][c][1] += uf[r][cp][1]; } } int stoi(int row, int col) { int acc = 0; for (int i = col; i < lines[0].length; i++) { int? d = isDigitMem(row, i); if (d != null) { acc = (acc * 10) + d; union(row, col, i); } else { break; } } return acc; } int? stoiSearch(int row, int col) { assert(row >= 0 && col >= 0 && row < lines.length && col < lines[0].length); if (isDigitMem(row, col) == null) { return null; } int i = col - 1; while (i >= 0) { if (isDigitMem(row, i) == null) { return stoi(row, i + 1); } i--; } return stoi(row, 0); } List> s2i = [for (int i = 0; i < lines.length; i++) Map()]; int? stoiSearchMem(int row, int col) { return s2i[row].putIfAbsent(col, () => stoiSearch(row, col)); } int count = 0; for (int i = 0; i < lines.length; i++) { for (int j = 0; j < lines[0].length; j++) { if (lines[i][j] != "*") { continue; } List gearVals = List.empty(growable: true); for (int x = -1; x <= 1; x++) { if (i + x < 0 || i + x > lines.length) { continue; } Set parents = {}; for (int y = -1; y <= 1; y++) { if (j + y < 0 || j + y > lines[0].length) { continue; } int? cur = stoiSearchMem(i + x, j + y); int parent = find(i + x, j + y); if (parents.contains(parent)) { continue; } parents.add(parent); if (cur != null) { gearVals.add(cur); } } } if (gearVals.length == 2) { count += gearVals[0] * gearVals[1]; } } } print(count); }
3b redux
I took out the union find, the code is simpler and more readable now. I also leaned in to using null values, which is gross but whatever, it works.
void day3s() { List lines = [ for (String? line = stdin.readLineSync(); line != null; line = stdin.readLineSync()) line ]; // lazy processing + memoization List> digs = [for (int i = 0; i < lines.length; i++) Map()]; int? isDigitMem(int r, int c) { if (r < 0 || r > lines.length || c < 0 || c > lines[0].length) return null; return digs[r].putIfAbsent(c, () => isDigit(lines[r][c])); } int stoi(int row, int col) { int acc = 0; for (int i = col; i < lines[0].length; i++) { int? d = isDigitMem(row, i); if (d != null) { acc = (acc * 10) + d; } else { break; } } return acc; } int? stoiSearch(int row, int col) { assert(row >= 0 && col >= 0 && row < lines.length && col < lines[0].length); if (isDigitMem(row, col) == null) { return null; } int i = col - 1; while (i >= 0) { if (isDigitMem(row, i) == null) { return stoi(row, i + 1); } i--; } return stoi(row, 0); } List> s2i = [for (int i = 0; i < lines.length; i++) Map()]; int? stoiSearchMem(int row, int col) { if (row < 0 || row >= lines.length) return null; if (col < 0 || col >= lines[0].length) return null; return s2i[row].putIfAbsent(col, () => stoiSearch(row, col)); } int count = 0; for (int i = 0; i < lines.length; i++) { for (int j = 0; j < lines[0].length; j++) { if (lines[i][j] != "*") { continue; } List gearVals = List.empty(growable: true); for (int x = -1; x <= 1; x++) { int? left = stoiSearchMem(i + x, j - 1); int? mid = stoiSearchMem(i + x, j); int? right = stoiSearchMem(i + x, j + 1); if (isDigitMem(i + x, j) == null) { if (left != null) { gearVals.add(left); } if (right != null) { gearVals.add(right); } } else if (left != null) { gearVals.add(left); } else if (mid != null) { gearVals.add(mid); } else if (right != null) { gearVals.add(right); } } if (gearVals.length == 2) { count += gearVals[0] * gearVals[1]; } } } print(count); }
6 a,b
Very easy today. You don’t need any programming to solve this; it’s a series of inequalities. That said, the brute force calculation is fast enough if you don’t want to think and is quite honestly faster than solving this mathematically.
So that being said, here’s the mathematic solution:
The inequality to solve is just: x = time holding the button
x^2 - tx + d < 0
which occurs when x is between (t/2) +/- (t^2 - 4d). The total ways are the total integers between those two numbers, exclusive.
Writing that all in code is a little tricky thanks to int/double rounding/truncation intricacies, so the brute force solution is “better”.
General
I used a class this time because it looked like it might be helpful. I don’t think it turned out to be that useful. Still, you can see Dart’s interesting constructor and getter syntax on display.
a.
Pretty straightforward, though I didn’t read the format correctly and had the destination/source data reversed. Oops! Luckily, my performance here will in no way affect my future career.
b.
I didn’t read the prompt correctly, which tripped me up. Also, while my solution is correct, it assumes that the input could be trickier than what the problem threw at me. Specifically, the edge case I had in mind was a range of values split into many subintervals and needing to track those mappings. I threw in some print statements to discover that intervals were never split beyond two subintervals, which was disappointing. Oh well- being correct is the best feeling you can have if you are otherwise empty inside.
Other than processing the input in the form of intervals, I don’t think there were any notable tricks at play here, so this was more of an exercise in implementing code cleanly, which I struggle with.
7 Camel Cards
a, b
I decided to write some classes and enums for this, which paid off a little in part b) when I could reuse most of my code.
9
a,b
This was another implementation exercise, i.e., can you implement this algorithm as specified? So pretty easy. a) took me about 30 minutes or so to code up, b) took like a minute after that since the problem is so similar.
Interestingly, the problem itself is based on a method for finding closed-form general formulae for progressions of integers. You can use it to develop a formula for sums of squares, cubes, quartics etc. or any progression that doesn’t seem to have an obvious closed form.
Anyway, there is probably a language where this problem can be solved with a one-liner, I got kinda close, see 9.dart. Check the commits if it’s unreadable.
8
Hint for b
The brute solution will take ~100 hours on my machine. You will need (to fudge) some very basic number theory to make it faster.
a
A straightforward implementation of the traversal was sufficient for performant code.
b
As suggested by the hint, I tried running the brute force solution. The pseudocode is something like this:
count = 0 while(all ghosts not on endpoint): for (all ghosts): move ghost to next node count++ print count
I put a timestamp for every 100mil iterations, which ticked once every two seconds.
So, how do we solve it? As mentioned in my hint, some fudged number theory is required.
Long story short, each ghost will eventually reach an end node. They could reach K endpoints, where K is the total number of end nodes available. They will follow the same path in a loop if they traverse infinitely.
The fudge is this: assume each ghost only hits one end node repeatedly. In this case, the solution is just the LCM of the number of steps it takes for each ghost to reach its end node from the initial state.
If given a case where a ghost hits multiple unique endpoints, you can probably find the configuration of counts and LCMs to yield the smallest one, but that proof is left to the reader.
The answer that I got was in the magnitude of 10 trillion, close to 20 trillion.