AoC 2023 D4P1: List Searching

Day 4′s problem asks us to find, on each line of input, how many members of the second list are members of the first list:

Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11

O’Reilly’s Perl Cookbook has concise code for finding the union and intersection of two lists, but it requires that each list has unduplicated entries. I suspect that’s going to be the case here but I’m not sure I should presume, so I’ll do my own thing.

my $numlistre = qr/(?:\d+\s+)*\d+/;

Each line of input has two lists of numbers on it that I’ll want to capture when parsing the line; and I could write the regular expression for capturing a list of numbers twice; but instead I’ll write it once (outside the loop, for efficiency) and put it in a variable.

This is zero or more sets of ( one or more digits followed by one or more spaces ) and then one more set of digits. To repeat * the digits-and-spaces, they need to be enclosed in ( ); but normally that captures and saves the contents and at this point we want to capture the entire list of numbers, not individual numbers in the list. (?: starts a non-capturing block that just groups the contents and doesn’t capture.

my ($winning, $mine) = /($numlistre)\s+\|\s+($numlistre)/
or die "didn't parse line $.:\n$_";

Don’t even bother matching the card number; just grab the two lists of numbers (as text).

my (@winning) = split(/\s+/, $winning);

Split the string of winning numbers on whitespace and save the result in an array.

my $value = 0;
$value = $value ? 2 * $value : 1
foreach grep { my $num = $_; grep { $_ == $num } @winning }
split(/\s+/, $mine);

$sum += $value;

Working backwards: Split the string of my numbers into a list, just as the winning numbers were split previously. Do something (grep) with that list. For each element of the list returned by grep, advance the value of this line — double it if it’s already non-zero, or advance it to 1 if it’s zero.

Now the grep, working from the outside in. Perl’s grep filters a list based on arbitrary criteria, returning the list of matching elements. So the outer grep loops through all of “my” numbers and returns the list of ones matching the condition inside the block.

In the block, grep sets $_ to each element being examined. Since I’m nesting greps, I cache the current value of the outer grep in $num. I then filter the list @winning of winning numbers looking for any that are $num — in other words, does the number I’m looking at from my list appear in the list of winning numbers?

If this number from my list appears in the list of winning numbers, then the inner grep will return a list containing just that number, or the scalar 1, as the result of the condition block. Either evaluates as true, so this number from my list will be included in the result of the outer grep. The outer grep thus returns the list of my numbers that are winners; and the foreach loop cycles through them advancing the value of the current ticket.

Full Program

#!/usr/bin/perl

use warnings;
use strict;

my $sum;

my $numlistre = qr/(?:\d+\s+)+\d+/;

while (<>) {
my ($winning, $mine) = /($numlistre)\s+\|\s+($numlistre)/
or die "didn't parse line $.:\n$_";
my (@winning) = split(/\s+/, $winning);

my $value = 0;
$value = $value ? 2 * $value : 1
foreach grep { my $num = $_; grep { $_ == $num } @winning }
split(/\s+/, $mine);

$sum += $value;
}

print "sum: $sum\n";

Leave a Reply