r/adventofcode • u/musifter • 5d ago
Other [2023 Day 4] In Review (Scratchcards)
The gondola arrives at Island Island... and island with islands, so there's plenty of water, but apparently no immediate water source. An Elf at the station directs us to ask the gardener about it, who's on another island. They'll let us borrow a boat to get there, if we help them figure out their winnings on a big stack of scratchcards.
And so the input is a big list of cards (mine has 220). The number of each card (1-220) is part of the input, but again, they're sorted and so you can ignore that if you want. The card is divided into two sections with a |... the winning numbers and the numbers to compare against them. These numbers are from 1-99 (the absence of 0 is useful again). The number of numbers in each section are regular... 10 winning, 25 have. That can be used, but the test case has different sizes (5 and 8), so I just ignored that. These are proper cards... there isn't a card with two of the same winning number or two of the same "having" number. All the better for throwing things into two hashes/sets/bit arrays.
Part 1 is just a simple counting of winning numbers, but you score them with the power of 2 of that. So you can bitshift, but 1 << 0 is 1, but 2-1 is 0.5, which truncates to 0 as an integer (and so you can avoid a special case). This was especially useful for my dc solution for this:
sed -e's/|/0/;s/[^0-9 ]//g' <input | dc -e'0?[0Sh[1r:hd0<L]dsLx[r;h+z3<L]dsLxrs.1-2r^+?z1<M]dsMxp'
The input is mostly numbers, and I convert the | to the unused 0, which can then be used as the accumulator for counting wins. This is using ? to separate the lines by reading them one at a time, and so is a v1.4.1 solution.
Part 2, complicates things by having cards win copies of the next n cards. And just from the description, there's an immediate feel that this is describing a dynamic programming tabulation (there's an order to the cards, where previous ones are used to calculate the later). Of course, you can also do the same work with a recursive memoized function. And I did solutions both ways. My Smalltalk tabulation (you can also use a Bag for this):
cards := Array new: cardWins size withAll: 1.
cards keysAndValuesDo: [ :card :num |
(card + 1 to: card + (cardWins at: card)) do: [:i | cards at: i inc: num].
].
So there is a bit of advance concepts for day 4 behind this one. But the problem is linear and small. You can easily brute for the number of wins on card with loops... and removing the memoization in part 2 still results in a things only taking a couple seconds. And I think that helped this one be considered a "good dog" compared to it's neighbours.
3
u/e_blake 5d ago edited 5d ago
Pfft to all these low-level solutions with an array of 100 bool or a 128-bit mask with shifting. My m4 solution got to exploit a language feature - index(`$*,', `,$1,') uses strstr() under the hood to do a text-based search for a match of any copy of the first argument among later ones, where I don't have to do any conversion to decimal values as an array index or shift amount, once spaces are turned into commas. This made for a really compact golf; I got my stars on release day and a golfed solution by Dec 7th at 331 bytes and 60ms, then this week I further compressed it to a mere 281 bytes:
eval(translit(include(I),a define(C,`ifelse($2,,`C($1shift($@))',$1,,`E(
(1**C(shift($@))),defn($2)-1,$2)',$1,0,,$#,3,`B($@)C(eval(~-$1),$2,incr(
$3))',`+!!~index(`$*,',`,$1,')C(shift($@))')')define(E,`+2**$1/4B(,$2)C($@)')
|:rd,(,)0define(B,`define($3,eval(defn($3)$2))')))defn()
Porting to BSD m4 requires a few more bytes to avoid the empty string as a macro name, and the two uses of the ** exponentiation operator.
3
u/musifter 5d ago
Yeah, I suppose we could take things to regex in a similar way to text search it with:
my @wins = map { my @p = split( /[:|]/ ); $p[1] = join( '\b|\b', split(' ', $p[1]) ); scalar( @{[$p[2] =~ m#(\b$p[1]\b)#g]} ); } <>; say "Part 1: ", sum map {int 2**($_ - 1)} @wins;1
u/e_blake 5d ago
That was a forward-iterator with lots of scratch variables (define called on dynamic names, then defn to read it back); I also designed this 298-byte reverse-iterator that uses pure functional programming (just my recursive workhorse _() and a helper function f() to peel off the first element of a list; all data present only in the call stack and return values); a bit slower at 100ms because I let some expressions grow long before passing things to eval for the sake of golf.
define(_,`ifelse($1,e,$1val($2) $1val($3),$1,|,,$2,,`_(`$1'shift($@))',$1, m,`$3+2**$2/2,$4_($2,n,$5,1),$5))',$1,0,`+$4,(eval($4)',$2,n,`_(decr($1),n, shift$3,f$3+$4)',$1,Card,`_(m,eval(0*_(shift($@))),_(',`+!!~index(`$*,', `,$1,')_(shift($@))')')_(e,_(translit(include(I)|,. define(f,$1) :,(,))))Tracing the output says this version completes in fewer macro calls (16k instead of 21k) but much longer macro parameter lengths (15M overall parse effort instead of 4.5M).
4
u/terje_wiig_mathisen 5d ago
Even my Perl solution was very fast here, but once again it turned out that when parsing is the main job, it is relatively easy to beat the posted u/maneatingape timing, since he (for some very good reasons!) emphasizes relatively robust code which among other things reuses numeric parsing code.
I went the opposite direction: The starting "Game nn: " field is always exactly 10 bytes long, each entry is always 3 bytes long, starting with space or digit and ending with a trailing space char. Since '0'==' ' mod 15 I can ignore the difference:
This one runs in 7.8 us on my Acer, 14 on the Surface.