One day is enough for me though, writing deliberately off-the-wall code is surprisingly mentally taxing, and normally I make sure to never program outside of work.
honestly I considered writing a parser-based approach
then I was too tired and thought "hmm, doing this with grok would be funny", but I didn't have logstash handy and fuck dealing with containers at midnight
#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
my $total = 0;
for my $line (<>) {
my @nums = ($line =~ /\d/g);
$total += $nums[0] * 10 + $nums[-1];
}
say $total;
part 2
perl
#!/usr/bin/env perl
use strict;
use warnings;
use v5.010;
my %nums = (one => 1, two => 2, three => 3, four => 4, five => 5, six => 6, seven => 7, eight => 8, nine => 9);
$nums{$_} = $_ for 1..9;
my $regex = join "|", keys %nums;
my $total = 0;
for my $line (<>) {
$line =~ /($regex)/;
my $first_num = $nums{$1};
my $window = 1;
my $sub = substr $line, -1;
while ($sub !~ /($regex)/) {
$window ++;
$sub = substr $line, -$window;
}
$sub =~ /($regex)/;
my $second_num = $nums{$1};
$total += $first_num * 10 + $second_num;
}
say $total;
Part 2 gave me a surprising amount of trouble. I resolved it by looking at longer and longer substrings from the end of the line in order to find the very last word even if it overlapped, which you can't do with normal regex split. I doubt this is the most efficient possible solution.
Also Lemmy is eating my < characters inside code blocks, which seems wrong. Pretend the "<>" part says "<>", lol
#!/usr/bin/env perl
use strict;
use warnings;
use v5.010;
use List::Util qw/ max /;
# Parse the input
my %games = ();
for my $line (<>) {
$line =~ /Game (\d+): (.+)/;
my $game_id = $1;
my $game_str = $2;
my @segments = split '; ', $game_str;
my @game = ();
for my $segment (@segments) {
my @counts = split ', ', $segment;
my %colors = (red => 0, blue => 0, green => 0);
for my $count (@counts) {
$count =~ /(\d+) (\w+)/;
$colors{$2} = $1;
}
push @game, { %colors };
}
$games{$game_id} = [ @game ];
}
# Part 1
my $part1 = 0;
game: for my $game_id (keys %games) {
for my $segment (@{$games{$game_id}}) {
next game if $segment->{red} > 12 || $segment->{green} > 13 || $segment->{blue} > 14;
}
$part1 += $game_id;
}
say "Part 1: $part1";
# Part 2
my $part2 = 0;
for my $game (values %games) {
my ($red, $green, $blue) = (0, 0, 0);
for my $segment (@$game) {
$red = max $segment->{red}, $red;
$green = max $segment->{green}, $green;
$blue = max $segment->{blue}, $blue;
}
$part2 += $red * $green * $blue;
}
say "Part 2: $part2";
After yesterday' fiddle-fest we are back with a straight-forward puzzle. Today we get the return of Manhattan distance, an AoC fav, but this time not spelled out to fool the crafty LLMs.
I made the initial decision not to "move" the galaxies in the initial map, but instead to store an offset that was increased whenever an empty row or column preceding the object was detected. This turned out to make part 2 really easy once I figured out the off-by-one error.
In retrospect that would have been far better for runtime, my dist function ended up being a tad expensive.
I substituted the rows/columns, with multiplication by the expansion rate if they were all numbers.
And then for each galaxy pair do a running sum by going “down” the “right” and adding the distance for each row and column crossed.
Late to the party and never done advents before, I liked how this problem reminded me that tree traversal is thing, almost as much as I don't that so much of my career involves powershell now.
Using command abbreviations like % and ? to keep the horizontal length friendly to lemmy post areas, they are expanded in git.
Part 2 in Powershell
function calculate([string]$data) {
# code for parsing data and calculating matches from pt1 here, check the github link if you like banal regexps
# returns objects with the relevant fields being the card index and the match count
}
function calculateAccumulatedCards($data) {
$cards = calculate $data # do pt1 calculations
$cards
| ? MatchCount -gt 0 # otherwise the losing card becomes its own child and the search cycles to overflow
| % {
$children = ($_.Index + 1) .. ($_.Index + $_.MatchCount) # range of numbers corresponding to indices of cards won
| % { $cards[$_ - 1] } # map to the actual cards
| ? { $null -ne $_ } # filter out overflow when index exceeds input length
$_ | Add-Member -NotePropertyName Children -NotePropertyValue $children # add cards gained as children property
}
# do depth first search on every card and its branching children while counting every node
# the recursive function is inlined in the foreach block because it's simpler than referencing it
# from outside the parallel scope
$cards | % -Parallel {
function traverse($card) {
$script:count++
foreach ($c in $card.Children) { traverse($c) }
}
$script:count = 0 # script: means it's basically globally scoped
traverse $_
$script:count # pass node count to pipeline
}
| measure -sum
| % sum
}
#!/usr/bin/env jq -n -R -f
# Dish to grid
[ inputs / "" ]
# Tilt UP
| transpose # Transpose, for easier RE use
| map( #
("#" + add) | [ # For each column, replace '^' with '#'
scan("#[O.]*") | [ # From '#' get empty spaces and 'O' rocks
"#", scan("O"), scan("\\.") # Let gravity do it's work.
] #
] | add[1:] # Add groups back together
) #
| transpose # Transpose back
# For each row, count 'O' rocks
| map(add | [scan("O")] | length)
# Add total load on "N" beam
| [0] + reverse | to_entries
| map( .key * .value ) | add
Similarly tired with index fiddling, I was pretty happy with my approach, which led to satisfying transpose cancelling in part 2. Not the fastest code out there, but it works. Day 14 was actually my favorite one so far ^^.
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.
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;
}
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).
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 =
0 if M_n = 0
sum of T_i, where i = n + 1 ... n + M_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);
}
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:
iterate through the characters to find a * symbol
Search the characters around it for a digit.
Get the value of the number associated with that digit by searching backwards until you find the start of a number
Use union-find to track whether or not you've seen this number before (because you can't assume that the same value is the same number)
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);
}
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.
Not my best work. No code in this comment, just check 5.dart if you want to see it in the github repo linked above.
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.
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".
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.
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.
I found this really tough for a day 1 problem. Because I don't really know regex, I did not know the secret to finding overlapping patterns. I quickly figured out that "twone" was a possibility, because it was in the example, but then I had to find other examples and hardcode them into my split pattern.
This isn't general, my input doesn't have 'sevenine' for example.
#!/usr/bin/env jq -n -R -f
# Get and reduce every "pretty" line
reduce inputs as $line (
0;
# Add extracted number
. + ( $line / "" | [ .[] | tonumber? ] | [first * 10 , last] | add )
)
First part was easy, and very suited to jq
Part 2
#!/usr/bin/env jq -n -R -f
# Define string to num value map
{
"one": 1, "1": 1,
"two": 2, "2": 2,
"three": 3, "3": 3,
"four": 4, "4": 4,
"five": 5, "5": 5,
"six": 6, "6": 6,
"seven": 7, "7": 7,
"eight": 8, "8": 8,
"nine": 9, "9": 9
} as $to_num |
# Get and reduce every "pretty" line
reduce inputs as $line (
0;
. + (
$line |
# Try two capture two numbers
capture("(^.*?(?(one|two|three|four|five|six|seven|eight|nine|[1-9])).*(?(one|two|three|four|five|six|seven|eight|nine|[1-9])).*?$)?") |
# If no capture, get one number twice
if .f == "" then $line | capture("^.*?(?(one|two|three|four|five|six|seven|eight|nine|[1-9]))") | .l = .f else . end |
# Add extracted number
$to_num[.f] * 10 + $to_num[.l]
)
)
Second part was harder than expected, i had to resort to regex.
Day 2
Part 1
#!/usr/bin/env jq -n -R -f
# For each game: Is 12 red cubes, 13 green cubes, and 14 blue cubes possible ?
# Line Format =
# Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
[
# Splitting input game id and content
inputs / ": " |
# Saving id
(.[0] / " " | .[1] | tonumber ) as $id |
# Parsing game
.[1] / "; " | [
.[] / ", " | [ .[] / " " | {(.[1]): .[0] | tonumber} ] | add |
# Is given sample possible ?
.red <= 12 and .green <= 13 and .blue <= 14
] |
# If all samples possible, return id, else 0
if all then $id else 0 end
] |
# Return sum of all possible game ids
add
Not too much trickery in this example.
Part 2
#!/usr/bin/env jq -n -R -f
# Line Format =
# Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
[
# Splitting input game id and content
inputs / ": " |
# Parsing game
.[1] / "; " |
[ .[] / ", " | [ .[] / " " | {(.[1]): .[0] | tonumber} ] | add ] |
# Getting minimum required mumber for each color,
# and computing the power
{
r: ([.[].red] | max),
g: ([.[].green] | max),
b: ([.[].blue] | max)
} | .r * .g * .b
] |
# Return sum of all powers
add
Satisifyingly straightfoward edit form part one.
Day 3
Part 1
#!/usr/bin/env jq -n -R -f
# Getting input with padding, and padded width
[ "." + inputs + "." ] as $inputs | ( $inputs[0] | length ) as $w |
# Working with flattened string, convert all symbols to '#'
[
([range($w) | "."]|join("")), # Padding
$inputs[],
([range($w) | "."]|join("")) # Padding
] | join("") | gsub("[^0-9.]";"#") as $inputs |
reduce (
# Get all indices for symbols, in box pattern around symbols
$inputs | indices("#")[] |
. - $w -1 , . - $w , . - $w + 1 ,
. - 1 , empty , . + 1 ,
. + $w - 1 , . + $w , . + $w + 1
) as $i (
# Numbers containes bounding indices,
# of numbers bordering symbols
{numbers: []};
# Test if current index isn't included in any found number
def new_number($i): [ .numbers[] | .[0] <= $i and $i <= .[1] ] | any | not ;
# Make "number" as bounding indices, by extending left and right
def make_number($i):
{a: $i, b: ($i+1 )}
| until( $inputs[.a:.b] | test("^[^0-9]"); .a -= 1 )
| until( $inputs[.a:.b] | test("[^0-9]$"); .b += 1 )
| [ .a +1 , .b -1 ]
;
# Add numbers if bordering symbol and new
if ($inputs[$i:$i+1] | test("[0-9]")) and new_number($i) then .numbers += [ make_number($i) ] else . end
) |
# Output sum of all found numbers
[ .numbers[] | $inputs[.[0]:.[1]] | tonumber ] | add
Took More time than i expected, glad i had the idea early to search by the indices of the symbols and not the digits.
Not super well suited to jq, unless I'm missing a better solution.
Part 2
#!/usr/bin/env jq -n -R -f
# Getting input with padding, and padded width
[ "." + inputs + "." ] as $inputs | ( $inputs[0] | length ) as $w |
# Working with flattened string, only keep gear '*' symbols
[
([range($w) | "."]|join("")), # Padding
$inputs[],
([range($w) | "."]|join("")) # Padding
] | join("") | gsub("[^0-9*]";".") as $inputs |
# Iterate over index positions of all gears
reduce ($inputs | indices("*")[]) as $i (
0;
# Re-use part-1 functions
def new_number($i):
[ .numbers[] | .[0] <= $i and $i <= .[1] ] | any | not
;
def make_number($i):
{a: $i, b: ($i+1 )}
| until( $inputs[.a:.b] | test("^[^0-9]"); .a -= 1 )
| until( $inputs[.a:.b] | test("[^0-9]$"); .b += 1 )
| [ .a +1 , .b -1 ]
;
# Reset and add numbers for each "box" ids
def add_numbers($box_idx):
reduce $box_idx[] as $i ({numbers:[]};
if ($inputs[$i:$i+1] | test("[0-9]")) and new_number($i) then
.numbers += [ make_number($i) ]
else
.
end
)
;
add_numbers([
$i - $w -1 , $i - $w , $i -$w + 1 ,
$i - 1 , empty , $i + 1 ,
$i + $w - 1, $i + $w , $i + $w + 1
]).numbers as $numbers |
if $numbers | length == 2 then
# Add product if exactly two bordering numbers
. += ( $numbers | map($inputs[.[0]:.[1]]|tonumber) | .[0] * .[1] )
else
.
end
)
Historically problems on Sat/Sun have been more challenging than weekdays. However given that the first 7 days are usually “warmup” problems, I’d say this years edition of AoC is more challenging than at least since 2019.
Pretty straightforward. Could probably be done in a few lines with the right syntactic sugar.
B
This is some game of life thing that I've never implemented or seen an implementation of, so I am altogether lost.
My current code (https://github.com/Fluxward/aoc2023/blob/main/21.dart) has a memoisation based approach but my current ailments are preventing me from really thinking about this properly so I am bowing out until I have the wherewithal.
This is the hardest problem of the year so far, based on leaderboard completion times. I'm busy wrapping up work for this year, and looking for a new job, so this will have to be put on the TODO pile
At this point I have officially given up and started looking at other people’s code. I’ll work on it after Imm fully better, it’s too much for me right now.
Through glancing at someone else's code, I was inspired to just try simulating the A problem beyond 64 steps and seeing the result.
Essentially it reaches a (bi stable?) steady state between two numbers, which makes sense- if you can only make single rook steps, then the reachable squares will alternate every cycle.
Don't know if I'll try solve this again tonight but mentally I have now understood the solution.
Update to the update: now fully recovered, I am now trying to finish the last problems.
Solved 21 B!
I spent way too much time on this but it’s fine
So my approach to AOC has always been to write a pure coding solution, which finally broke down here.
First, the solve:
I call the unrepeated garden map the “plot”. Each repetition of the plot I call a “grid”. Hope that isn’t confusing.
Looking at the input data, it is a grid of 131x131 squares with the starting position in the center at 66,66 (indexed from 1)
If you imagine a chessboard pattern over the whole area, on each step the possible reachable squares alternate colors.
Row 66 and col 66 are unimpeded by stones, as well as the edges of each grid. This means that:
starting from the initial grid, it takes 66 steps to enter a new grid. You enter a new grid on all sides.
a grid is always initially entered from the middle of an edge, either on one or two sides based on its position relative to the grids that are currently being considered.
each grid is independent of every other grid, except for the step where it is entered.
To see why that last point is true, consider that in order for another grid A to influence an adjacent grid B beyond the moment the adjacent grid is entered, there must be a reachable point further from the midpoint of the edge on the edge of A. However, because the middle row and column are free from rocks, this is never the case. Any influence from A reaches B too late, i.e. reachable squares on B from A will be reachable sooner from just travelling from the entry point on B.
The number of steps is 131x100x2023 + 65.
So putting all this together, the way I got the answer was like this:
Simulate the whole area for 65 + (5*131) steps (more than necessary)
Print the number of reachable squares in the grid at 65 + 131n for n = 0-5
Using pen and paper, determine some coefficients for some of the numbers that come up, add everything up in a calculator, and type that answer into AOC.
So I guess the answer I arrived at was what I’d been thinking I should be doing this whole time: a mix of simulating some of the problem and a decent amount of pen and paper work to get the solution out, rather than just pure coding. Fun!
Sorry for the necropost: I have completed all the problems! One of them completely stumped me and I had to cheat. Not going to do a writeup unless requested :)
24b, sort of. The problem came down to “hey do you remember how to do linear algebra?” and the answer was: dawg, I barely know normal algebra. I had solved the vibes of the problem but none of the details, though.
Happy Holidays everyone. I’ve decided I am going to take a break from aoc to properly rest and recover from my mystery illness. Perhaps I will attempt solves again in the new year.
Thanks dawg. AOC has occupied the working part of my brain but now that it requires more brain wrinklies than usual I’m gonna go back to writing sneers.
I liked the slight trickiness of part 2, that the naive implementation would never complete in time.
As always doing a JQ implementation:
Part 1
#!/usr/bin/env jq -n -R -f
# Get seeds
input | [ match("\\d+"; "g").string | tonumber ] as $seeds |
# Collect maps
reduce inputs as $line ({};
if $line == "" then
.
elif $line | test(":") then
.k = ( $line / " " | .[0] )
else
.[.k] += [[ $line | match("\\d+"; "g").string | tonumber ]]
end
)
# For each map, apply transformation to all seeds.
# seed -> ... -> location
| reduce ( to_entries[] | select(.key != "k") .value) as $map ({s:$seeds};
.s[] |= (
# Only attempt transform if element is in one of the ranges
[ . as $e | $map[] | select(. as [$d,$s,$l] | $e >= $s and $e < $s + $l) ] as $range |
if ($range | length ) > 0 then
$range[0] as [$d,$s] |
. - $s + $d
else
.
end
)
)
# Get lowest location
| .s | min
Some comments:
A nice use of input first to get the seeds, then inputs to get remaining lines.
Part 2
#!/usr/bin/env jq -n -R -f
# Utility function
def group_of($n):
( length / $n ) as $l |
. as $arr |
range($l) | $arr[.*$n:.*$n+$n]
;
# Get all seed ranges
input | [ match("\\d+"; "g").string | tonumber ] | [group_of(2)] as $seeds |
# Collect maps
reduce inputs as $line ({};
if $line == "" then
.
elif $line | test(":") then
.k = ( $line / " " | .[0] )
else
.[.k] += [[ $line | match("\\d+"; "g").string | tonumber ]]
end
)
# For each map, apply transformation to all seeds ranges.
# Producing new seed ranges if applicable
# seed -> ... -> location
| reduce (to_entries[] | select(.key != "k") .value) as $map ({s:$seeds};
.s |= [
# Only attempt transform if seed range and map range instersect
.[] | [.[0], add, .[1] ] as [$ea, $eb, $el] | [
$map[] | select(.[1:] | [.[0], add ] as [$sa,$sb] |
( $ea >= $sa and $ea < $sb ) or
( $eb >= $sa and $eb < $sb ) or
( $sa >= $ea and $sa < $eb )
)
] as $range |
if $range | length > 0 then
$range[0] as [$d,$s,$l] |
# ( only end ) inside map range
if $ea < $s and $eb < $s + $l then
[$ea, $s - $ea], [$d, $eb - $s ]
# ( both start, end ) outside map range
elif $ea < $s then
[$ea, $s - $ea], [$d, $l], [ $s + $l, $eb ]
# ( only start ) inside map range
elif $eb > $s + $l then
[$ea + $d - $s, $l - $ea + $s ], [$s + $l, $eb - $s - $l]
# ( both start, end ) inside map range
else
[$ea + $d - $s , $el]
end
else
.
end
]
)
# Get lowest location
| [.s[][0]] | min
Some comments:
Since iterating across all seeds naively would take forever, iterating over seed ranges instead.
It's nice that JQ can neatly produce extra elements: [1,2,3] | [ .[] | if . == 2 then . * 10 + 1 , . * 10 + 2 else . end ] -> [1, 21, 22, 3]
There is probably a more compact way of expressing all the conditions and produced outputs.
Replaced less-than (and greater-than for symmetry) symbols with full-width version, since lemmy apparently doesn't handle them well within a code block: replacing less than with <
So following from yesterday where I was punished by not going full OO, I decided, hey, this is a problem in which I can do some OOP, so I did. This took very long to do but I regret nothing. If you look at my code, feel free to click your tongue and shake your head at every opportunity I missed to use a design pattern.
Anyway, after a slight snafu with misunderstanding the FF logic and not spotting that some modules can be dummy modules, it all just worked, and I got my answer.
B
This was a bit of a headscratcher, but the answer was surprisingly simple.
First, the answer. Here's how to do it:
Look for the "rx" module in your input.
If the module that outputs to rx is a conjunction, keep track of how many button presses it takes for each input of the conjunction to change. The answer is the LCM of all those numbers.
If the module is a FF, you can also work backwards to get it, but this was not my input so I didn't need to try this.
Getting here was a bit weird. I hoped that I could just run the code from A and spit out the answer when rx went low, but as of time of writing I've been running it now on a separate machine for about an hour and still no result.
My next instinct was to simply work it out from pen and paper. I thought it might be possible (it probably is) but decided to at least experimentally see if the states of the modules connected to rx were cyclic first. I did, and that was enough for me to get to the answer.
My answer was about 230 trillion BPs, which, extrapolating on how long execution is taking on my other machine, might take just under 137 years to calculate naively. Fun!
Yeah, I mean it’s always possible to do without all the OO stuff, I just didn’t want to mentally keep tabs on what each variable or method did what. That’s what the code is for!
Completed when waiting for the second leg of my Christmas holidays flight. (It was a long wait, can I blame jet-lag?).
Have a more compact implementation of LCM/GCD, something tells me it will come in handy In future editions. (I’ve also progressively been doing past years)
I have come up with a more horrifying way to solve Day 1 Part 1 involving C++ implementation defined behavior, but it requires launching two subprocesses for every test case so I'm not sure I have the motivation right now.
p1 part 1 submitted, runs fine. part 2 says "wrong answer for you but right for someone else". doing a debug-print-everything run on the intermediate stages of the data after my regex all seems correct, but it's also 23h50 and it's been a looooong week. so I guess I'll take a look a fresh look at that one in the morning over coffee
part1, badly:
spoiler
import re
pattern = '\d'
cmp = re.compile(pattern)
# extremely lazy, of the ungood kind
datapath = 'data'
lines = open(datapath, 'r').read().split('\n')
candidates = []
values = []
for l in lines:
if l != '':
r = cmp.findall(l)
candidates.append(r)
values.append(int(''.join([r[0], r[-1]])))
print(candidates)
print(values)
print(sum(values))
(I really wasn't kidding about the badly)
part2:
spoiler
missed the eightwo case
changes:
mapping = {...} # name/int pairs
pattern = f'(?=(\d|{"|".join(mapping.keys())}))'
lines = open(datapath, 'r').read().split('\n').remove('')
values = []
for l in lines:
r = cmp.findall(l)
equivs = [str(mapping.get(x, x)) for x in r]
head, tail = [equivs[0], equivs[-1]]
values.append(int(f"{head}{tail}"))
print(sum(values))
Seeing you implement gcd/lcm makes me think about the people who are gunning for the AoC leaderboards. What do they have that I don’t?
Asking out of general interest and not any sort of feelings of inadequacy (I swear, behind a face of gritted teeth and obvious seethe).
Like, do they have cron scripts to scrape the prompt and input as soon as it comes out? Libraries of util functions accrued from years of AoC participation? That’s all I’ve thought of and honestly it doesn’t sound implausible if you are hypercompetitive. Like I imagine they just have a raiders of the lost ark warehouse of boilerplate indexed in their memory palace to draw from. And I don’t have that and I am totally not envious at all.
I just literally followed the instructions, and got a solution in 20ms. This despite literally creating each intermediate array yet only using the ends. I'm sure I used way to much memory but you know? I'm using a $5/mo VPS for everything and unless I'm barking totally up the wrong tree I've never exceeded its memory limits.
On the subreddit I see people discussing recursion and "dynamic programming" (which is an empty signifier imho) but I really don't see the need, unless you wanna be "elegant"
DP to me is when you use memoisation and sometimes recursion and you want to feel smarter about what you did.
I also struggle to think of the need for DP, even in a more “elegant” approach. Maybe if you wanted to do an O(n) memory solution instead of n^2, or something. Not saying this out of derision. I do like looking at elegant code, sometimes you learn something.
I feel like there’s an unreadable Perl one line solution to this problem, wanna give that a go, @gerikson?
a: Just copied what I did for 10 b. and applied it to work here. Had to fiddle with it to make it work, but it worked. I implemented a line-crossing counting algorithm. If you slice up the area into rows, you can tell if you are inside the area if you cross an odd number of lines that vertically cross that row.
b. Pretty much just changed the input parsing and ran the same algorithm. Thought that it might be too slow, but I put in some stopwatch ticks and found out that it should take about 5 minutes to run my a) algorithm to get the answer.
The actual time elapsed was 5m50s.
I miiiiight try make a faster version but my head is too fucked up to care right now, hence me letting the slow algorithm run.
It's not cheating if it's something you learned and forgot from a university course
So, I have made my code a million times faster. My original algorithm counted every square of area individually, plus a bunch of other inefficient things.
My new algorithm now uses some swanky computational geometry to calculate the enclosed area. It took a bit of scribbling on a paper pad to get this right. Some notes:
If you naively use the coordinates you land on as the points of a polygon and then calculate the area of that polygon, you will slightly underestimate the enclosed area.
The error comes from the contribution of the area of the perimeter. Drawing out the sample case and observing how it contributes, it basically contributes 1/2 m^3 for every straight portion of the perimeter, and 1/4 or 3/4 for a corner, depending on if that corner is a right or left turn. It should instead contribute 1 for each perimeter tile, so you need to calculate this difference.
You can use the cross product to determine if a set of 3 points forms a straight line, a left turn, or a right turn.
So, here's what I did. I looked at some old lecture notes I had on programming competition computation geometry implementation details. I copied and pasted some code that implemented a counter-clockwise check and one that calculated the area of a simple polygon. That's pretty much all I needed.
Now my code runs in a few milliseconds.
There is almost definitely a more elegant way to do what I did for this iteration but I'm happy to leave that to someone else to figure out (or copy paste from reddit or something).
After this problem, I will create a new reply in the OP if it is not there already, and will discuss under that thread.
a,b
So, like many other problems from this year, this is one of those direct solution problems where there isn't much of a neat trick to getting the answer. You just have to implement the algorithm they specify and hope you can do it correctly.
a) I used a regex to do some parsing because I haven't looked at dart regex much and wanted to dip my toes a little.
I considered doing this "properly" with OO classes and subclasses for the different rules. I felt that it would be too difficult and just wrote something janky instead. In hindsight, this was probably the wrong choice, especially since grappling with all the nullable types I had in my single rule class became a little too complex for my melting brain (it is HOT in Australia right now; also my conjunctivae are infected from my sinus infection. So my current IQ is like down 40 IQ points from its normal value of probably -12)
b) There may have been a trick here to simplify the programming (not the processing). Again, I felt that directly implementing the specified algorithm was the only real way forward. In brief:
Start with an "open" set containing a part with 4 ranges from [1, 4001) and an "accepted" set that is empty.
Start at the workflow "in"
For each rule in the current workflow:
If the rule accepts part of the ranges in the open set, remember those ranges in a closed set and remove them from the open set.
Remove anything the rule rejects from the open set.
If the rule redirects to a different workflow W, split off the applicable ranges and recurse at 3 with the current workflow as W.
Keep in the open set anything the rule doesn't consider.
Because this is AOC, I assumed that the input would be nice and wouldn't have anything problematic like overlapping ranges, and I was right. I had a very stupid off by one error that took me a while to find as well.
The code I have up as of this comment is pretty long and boring, I might try clean it up later.
I have contracted some kind of sinus infection, so rn I have a -20 IQ debuff.
a,b
part a: nothing to say here.
part b:
Before diving into the discussion, I timed how long 1000 cycles takes to compute, and apparently, it would take 1643175 seconds or just over 19 days to compute 1 billion cycles naively. How fun!
So, how do you cut down that number? First, that number includes a sparse map representation, so you can tick that box off.
Second is intuiting that the result of performing a cycle is cyclical after a certain point. You can confirm this after you've debugged whatever implementation you have for performing cycles- run it a few times on the sample input and you'll find that the result has a cycle length of 7 after the second interaction.
Once you've got that figured out, it's a matter of implementing some kind of cycle detection and modular arithmetic to get the right answer without having to run 1000000000 cycles. For the record, mine took about 400 cycles to find the loop.
I took a very similar approach to parts a and b, with the difference that i was too lazy to do titling in each direction, and wanted to abuse regex so Instead i always titled up and rotated, which given my method of tilting up and rotating had some satisfying cancelling of transpose operations:
https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/14-b.jq
# Relevant portion
# oneCycle expects an array, of array of chars (3x3 eg: [[".","#","."],[".",".","."],["O",".","#"]])
def oneCycle:
# Tilt UP = T . MAP(scan) . T
# Rotate = T . MAP(reverse)
# Titl UP . Rotate = T . MAP(scan) . Map(reverse) | T . T = Identity
def tilt_up_rotate:
transpose # Gets subgroups # Within each group,
# starring with "#" # In order 1 "#", x "O", y "."
| map( ("#" + add) | [ scan("#[^#]*") | ["#", scan("O"), scan("\\.")] ] | add[1:])
| map(reverse)
;
# Tilt North, West, South, East
tilt_up_rotate | tilt_up_rotate | tilt_up_rotate | tilt_up_rotate
;
JQ does allow some nice sortcuts sometimes, again transpose is nice to have.
a:
So, I've been in the habit of skipping the flavour text and glossing over the prompt. This time, it hurt me badly.
I read the problem as follows: for N galaxies, find N/2 pairings such that the sum of distances is minimal.
At this point, I was like, wow, the difficulty has ramped up. A DP? That will probably run out of memory with most approaches, requiring a BitSet. I dove in, eager to implement a janky data structure to solve this humdinger.
I wrote the whole damn thing. The bitset, the DP, everything. I ran the code, and WOAH, that number was much smaller than the sample answer. I reread the prompt and realised I had it all wrong.
It wasn't all for naught, though. A lot of the convenience stuff I'd written was fine. Also, I implemented a sparse parser, which helped for b.
b:
I was hoping they were asking for what I had accidentally implemented for a. My hopes were squandered.
Anyway, this was pretty trivial with a sparse representation of the galaxies.
The hardest part has finding the right dart ascii library to use (by default dart treats everything as UTF-16, which is horrible for this sort of thing) and the right data structure (linked hash map, which is a map that remembers insertion order.)
Are you a bad enough dude to formulate this DP correctly?
Are you a bad enough dude to choose a good DP state storage schema?
Is your computer a bad enough dude to store all the DP states if you choose a bad storage schema?
...which is true of all DP problems, honestly. So given you know and understand how to approach DP problems, this would be more an engineering issue than anything.
So in my first year in university, in my intro DSA class, we learned A*. Have I learned any other ways to search since? Not really. So I used A* here.
It took way longer than it should have to solve, which I blame on my ongoing illness. The main sticking point was that I implemented a class to represent the search state, and since I was going to use it as a key to a map, I implemented a hashcode for it. The rest of the A* code freely flowed from my brain (really the wikipedia pseudocode).
Cue like 40 mins plus of wondering how the test input was searching over millions of states, wondering if I’d fucked up the A* implementation, wondering if the problem was too big for A*, and wondering if it was finally time to take a days break from all the aoc nonsense.
Anyway at some point I realised I forgot to implement the corresponding equals method to my hashcode. Once I had that my code ran in seconds and everything was fine. This sickness is the worst!!!
a was relatively straightforward - again, an implementation test.
b was interesting- luckily, I remembered how to test if a point lives inside a curve. The line-crossing count algorithm was a little tricky to write from scratch, but I finally got there. Also, I screwed up for like an hour when I didn't realise you were supposed to count junk pipes as part of the territory. Code wise it got messy, and I've thrown something up on my github, but might try clean it up a little later.
a. while you can brute force this one in time, one simple trick to make it faster is to treat the symbols as bits and interpret the grid as numbers. It then becomes a matter of locating the reflection point.
b. It's not much of a difference to solve b. The trick here is that if you did the bit stuff I suggested above, you'd quickly realise that a smudge interpreted as a binary number is a power of two. Finding a smudge is equivalent to if the bitwise XOR of two numbers is a power of 2, which can be done with some bitwise magic.
So, as I've been doing the AoC things, I've been creating small libraries of convenience functions to reuse, hopefully. This time, I reused some things I wrote for problem 10, which was vindicating.
a. was a fun coding exercise. Not much more to say.
b. I lucked out by making a recursive traversal function for a), which let me specify the entry and direction of where my traversal would start. Besides that, similar to a., this was a fun coding exercise. I was surprised that my code (which just ran the function from a) on every edge tile) It only took 2s to run; I thought I might need to memoize some of the results.
In my case it was a lot more of headbanging, the traverse function i wrote for part a was way to slow, since JQ isn't happy with loop-heavy assignments (if not buried within C-implemented builtins). Part a completed in ~2seconds, which was never going to do (in hindsight it would have taken me less time to simply let it run slowly), I had to optimize it so that the beams don't step one square at a time, but shoot straight to any obstacle.
It took me waaaay too long to troubleshoot it into something that actually worked. I'm sure there's a compact implementation out there, but my part b ended up looking very meaty (and still took ~30s to run):
https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/16-b.jq
Personal completion time: ahahahahahahaha (at least i had fun)
Where a curse the fact I decided to use JQ and not a "real" programming language.
spoiler
Had to resort to memoization, but sadly JQ isn't mega well suited to that. I had to refactor my part 1 function, to make including the "state" at every function call possible. I wish it were as easy as a @cache decorator, but i guess this way i had fun (for an arbitrary definition of "fun")
Also lost a fair amount of time not not noticing that the sequence should be joined with "?" not with "". (that'll teach me to always run on the example before the full input, when execution time is super long).
Execution time: 17m10s (without memoization a single row was taking multiple minutes, and there's 1000 rows ^^...)
EDIT: see massive improvement by running in parallel in reply.
Satisfyingly short (in lines, not in time writing) some of the longer part is hexadecimal parsing, that doesn't come natively in JQ,
I started doing polygon math from part 1, and what took me the longest was properly handling the area contributed by the perimeter.
(I toyed with trying very annoying things like computing the outmost vertex at each turn, which is complicated by the fact that you don't initially know which way the digger is turning, and needing previous and next point to disambiguate).
#!/usr/bin/env jq -n -R -f
reduce (
# Produce stream of the vertices, for the position of the center
foreach (
# From hexadecimal representation
# Get inputs as stream of directions = ["R", 5]
inputs | scan("#(.+)\\)") | .[0] / ""
| map(
if tonumber? // false then tonumber
else {"a":10,"b":11,"c":12,"d":13,"e":14,"f":15}[.] end
)
| [["R","D","L","U"][.[-1]], .[:-1]]
| .[1] |= (
# Convert base-16 array to numeric value.
.[0] * pow(16;4) +
.[1] * pow(16;3) +
.[2] * pow(16;2) +
.[3] * 16 +
.[4]
)
) as $dir ([0,0];
if $dir[0] == "R" then .[0] += $dir[1]
elif $dir[0] == "D" then .[1] += $dir[1]
elif $dir[0] == "L" then .[0] -= $dir[1]
elif $dir[0] == "U" then .[1] -= $dir[1]
end
)
# Add up total area enclosed by path of center
# And up the are of the perimeter, perimeter * 1/2 + 1
) as [$x, $y] ( #
{prev: [0,0], area: 0, perimeter_area: 1 };
# Adds positve rectangles
# Removes negative rectangles
.area += ( $x - .prev[0] ) * $y |
# Either Δx or Δy is 0, so this is safe
.perimeter_area += (($x - .prev[0]) + ($y - .prev[1]) | abs) / 2 |
# Keep current position for next vertex
.prev = [$x, $y]
)
# Output total area
| ( .area | abs ) + .perimeter_area
Satisfyingly very well suited to JQ once you are used to the stream, foreach(init; mod; extract) and recurse(exp) [where every output item of exp as a stream is fed back into recurse] operators. It's a different way of coding but has a certain elegance IMO. This was actually quick to implement, along with re-using the treating a range as a primitive approach of the seeds-to-soil day.
#!/usr/bin/env jq -n -sR -f
inputs / "\n\n"
# Parse rules
| .[0] / "\n"
| .[] |= (
scan("(.+){(.+)}")
| .[1] |= (. / ",")
| .[1][] |= capture("^((?<reg>.)(?<op>[^\\d]+)(?<num>\\d+):)?(?<to>[a-zA-Z]+)$")
| ( .[1][].num | strings ) |= tonumber
| {key: .[0], value: (.[1]) }
) | from_entries as $rules |
# Split part ranges into new ranges
def split_parts($part; $rule_seq):
# For each rule in the sequence
foreach $rule_seq[] as $r (
# INIT = full range
{f:$part};
# OPERATE =
# Adjust parts being sent forward to next rule
if $r.reg == null then
.out = [ .f , $r.to ]
elif $r.op == "<" and .f[$r.reg][0] < $r.num then
([ .f[$r.reg][1], $r.num - 1] | min ) as $split |
.out = [(.f | .[$r.reg][1] |= $split ), $r.to ] |
.f[$r.reg][0] |= ($split + 1)
elif $r.op == ">" and .f[$r.reg][1] > $r.num then
([ .f[$r.reg][0], $r.num + 1] | max ) as $split |
.out = [(.f | .[$r.reg][0] |= $split), $r.to ] |
.f[$r.reg][1] |= ($split - 1)
end;
# EXTRACT = parts sent to other nodes
# for recursion call
.out | select(all(.[0][]; .[0] < .[1]))
)
;
[ # Start with full range of possible sings in input = "in"
[ {x:[1,4000],m:[1,4000],a:[1,4000],s:[1,4000]} , "in" ] |
# Recusively split musical parts, into new ranges objects
recurse(
if .[1] == "R" or .[1] == "A" then
# Stop recursion if "Rejected" or "Accepted"
empty
else
# Recursively split
split_parts(.[0];$rules[.[1]])
end
# Keep only part ranges in "Accepted" state
) | select(.[1] == "A") | .[0]
# Total number if parts in each object is the product of the ranges
| ( 1 + .x[1] - .x[0] ) *
( 1 + .m[1] - .m[0] ) *
( 1 + .a[1] - .a[0] ) *
( 1 + .s[1] - .s[0] )
# Sum total number of possibly accepted musical parts
] | add
EDIT: Less-thans and greater-thans replaced by fullwidth version, because lemmy is a hungry little goblin.
In hindsight this is is searching for the shortest path in a graph, making sure that each square is actually two nodes, for when approached vertically or horizontally. My shcool days are distant now, and i wonder how far from optimal my implementation is ^^.
Part two was only a small edit from part one for me, and the code actually ran faster! from ~45s -> ~20s.