add advent of code examples (2020, days 1-5) (re #27)

FossilOrigin-Name: 54a80479319c1ce4c27fdae1169b09159fb624f6d10c5224a338273ec1ea160a
This commit is contained in:
crc 2020-12-18 20:44:56 +00:00
parent e61c0364df
commit 8b21ab556a
5 changed files with 811 additions and 0 deletions

View file

@ -0,0 +1,106 @@
# Day 1: Report Repair
After saving Christmas five years in a row, you've decided
to take a vacation at a nice resort on a tropical island.
Surely, Christmas will go on without you.
The tropical island has its own currency and is entirely
cash-only. The gold coins used there have a little picture
of a starfish; the locals just call them stars. None of the
currency exchanges seem to have heard of them, but somehow,
you'll need to find fifty of these coins by the time you
arrive so you can pay the deposit on your room.
To save your vacation, you need to get all fifty stars by
December 25th.
Collect stars by solving puzzles. Two puzzles will be made
available on each day in the Advent calendar; the second
puzzle is unlocked when you complete the first. Each puzzle
grants one star. Good luck!
Before you leave, the Elves in accounting just need you to
fix your expense report (your puzzle input); apparently,
something isn't quite adding up.
Specifically, they need you to find the two entries that
sum to 2020 and then multiply those two numbers together.
For example, suppose your expense report contained the
following:
1721
979
366
299
675
1456
In this list, the two entries that sum to 2020 are 1721
and 299. Multiplying them together produces
1721 \* 299 = 514579, so the correct answer is 514579.
Of course, your expense report is much larger. Find the
two entries that sum to 2020; what do you get if you
multiply them together?
----
The first step is to get the input into something I
can work with. My sample input has 200 entries, so I
can fit them all on the stack, which means it's easy
to create an array with them.
Put your input into a file named `input-day-1`.
~~~
{ 'input-day-1 [ s:to-number ] file:for-each-line }
'INPUT const
~~~
And then a simple approach is to just loop through the
input twice, looking for any two values that add up to
2020.
----
~~~
:2020? dup-pair + #2020 eq? ;
:display dup-pair * n:put nl ;
INPUT [ INPUT [ 2020? [ display ] if drop ] a:for-each drop ] a:for-each
~~~
The second part is just slightly different:
----
The Elves in accounting are thankful for your help; one
of them even offers you a starfish coin they had left
over from a past vacation. They offer you a second one
if you can find three numbers in your expense report that
meet the same criteria.
Using the above example again, the three entries that
sum to 2020 are 979, 366, and 675. Multiplying them
together produces the answer, 241861950.
In your expense report, what is the product of the
three entries that sum to 2020?
----
For this, I just add an additional iteration through the
input. This can be *slow*, but does work. To save time,
I `abort` after a match is found, so processing stops at
the first match.
~~~
:dup-three 'abc 'abcabc reorder ;
:2020? dup-three + + #2020 eq? ;
:display dup-three * * n:put nl abort ;
:inner INPUT [ 2020? [ display ] if drop ] a:for-each drop ;
INPUT [ INPUT [ inner ] a:for-each drop ] a:for-each
~~~

View file

@ -0,0 +1,143 @@
# Day 2: Password Philosophy
Your flight departs in a few days from the coastal
airport; the easiest way down to the coast from
here is via toboggan.
The shopkeeper at the North Pole Toboggan Rental
Shop is having a bad day. "Something's wrong with
our computers; we can't log in!" You ask if you can
take a look.
Their password database seems to be a little
corrupted: some of the passwords wouldn't have been
allowed by the Official Toboggan Corporate Policy
that was in effect when they were chosen.
To try to debug the problem, they have created a
list (your puzzle input) of passwords (according
to the corrupted database) and the corporate
policy when that password was set.
For example, suppose you have the following list:
1-3 a: abcde
1-3 b: cdefg
2-9 c: ccccccccc
Each line gives the password policy and then the
password. The password policy indicates the lowest
and highest number of times a given letter must
appear for the password to be valid. For example,
`1-3 a` means that the password must contain `a` at
least 1 time and at most 3 times.
In the above example, 2 passwords are valid. The
middle password, cdefg, is not; it contains no
instances of b, but needs at least 1. The first and
third passwords are valid: they contain one a or
nine c, both within the limits of their respective
policies.
How many passwords are valid according to their
policies?
----
This is an easy problem. Just a little parsing and reduction.
`range` is defined to parse the limits of the character. This
just means splitting the string and converting the pieces to
a number.
The `reduce` word is slighly more complex. It takes the
password and the character and constructs a combinator for
use in filtering out other characters.
The remaining characters will be counted and compared to
the range.
Basically, an input line like:
1-3 a: abcde
Becomes:
'abcde [ $a eq? ] s:filter s:length #1 #3 n:between?
At this point, I just add up the flags (-1 for TRUE), and
use `n:abs` to get the final count.
~~~
:range
#0 a:fetch $- s:tokenize [ s:to-number ] a:for-each ;
:reduce
[ #2 a:fetch ] [ #1 a:fetch fetch &eq? curry ] bi s:filter ;
:process (na-n)
[ reduce s:length ] [ range ] bi n:between? + ;
#0 'input-day-2 [ ASCII:SPACE s:tokenize process ] file:for-each-line
n:abs
~~~
----
# Part 2
While it appears you validated the passwords
correctly, they don't seem to be what the Official
Toboggan Corporate Authentication System is expecting.
The shopkeeper suddenly realizes that he just
accidentally explained the password policy rules from
his old job at the sled rental place down the street!
The Official Toboggan Corporate Policy actually works
a little differently.
Each policy actually describes two positions in the
password, where 1 means the first character, 2 means
the second character, and so on. (Be careful; Toboggan
Corporate Policies have no concept of "index zero"!)
Exactly one of these positions must contain the given
letter. Other occurrences of the letter are irrelevant
for the purposes of policy enforcement.
Given the same example list from above:
1-3 a: abcde is valid: position 1 contains a and position 3 does not.
1-3 b: cdefg is invalid: neither position 1 nor position 3 contains b.
2-9 c: ccccccccc is invalid: both position 2 and position 9 contain c.
How many passwords are valid according to the new
interpretation of the policies?
----
So in this variation, I need to check two positions per
password.
Being lazy here, I'm using `reorder` a couple of times
to restructure the stack. The only thing to remember is
that indexing is one based, so the word that extracts
the positions needs to decrement to adjust for Retro's
zero based indexing.
~~~
:grab-characters
'abc 'abac reorder + fetch [ + fetch ] dip ;
:check
'abc 'abac reorder eq? [ eq? ] dip xor + ;
:positions
#0 a:fetch $- s:tokenize [ s:to-number n:dec ] a:for-each ;
:process (na-n)
[ #1 a:fetch fetch ] [ #2 a:fetch ] [ positions ] tri
grab-characters check ;
#0 'input-day-2 [ ASCII:SPACE s:tokenize process ] file:for-each-line
n:abs
~~~

View file

@ -0,0 +1,178 @@
# Day 3: Toboggan Trajectory
With the toboggan login problems resolved, you set off
toward the airport. While travel by toboggan might be
easy, it's certainly not safe: there's very minimal
steering and the area is covered in trees. You'll need
to see which angles will take you near the fewest trees.
Due to the local geology, trees in this area only grow on
exact integer coordinates in a grid. You make a map (your
puzzle input) of the open squares (.) and trees (#) you can
see. For example:
..##.......
#...#...#..
.#....#..#.
..#.#...#.#
.#...##..#.
..#.##.....
.#.#.#....#
.#........#
#.##...#...
#...##....#
.#..#...#.#
These aren't the only trees, though; due to something you
read about once involving arboreal genetics and biome
stability, the same pattern repeats to the right many times:
..##.........##.........##.........##.........##.......
#...#...#..#...#...#..#...#...#..#...#...#..#...#...#..
.#....#..#..#....#..#..#....#..#..#....#..#..#....#..#.
You start on the open square (.) in the top-left corner and
need to reach the bottom (below the bottom-most row on your
map).
The toboggan can only follow a few specific slopes (you opted
for a cheaper model that prefers rational numbers); start by
counting all the trees you would encounter for the slope right
3, down 1:
From your starting position at the top-left, check the
position that is right 3 and down 1. Then, check the position
that is right 3 and down 1 from there, and so on until you go
past the bottom of the map.
The locations you'd check in the above example are marked here
with O where there was an open square and X where there was a
tree:
..##.........##.........##.........##.........##.......
#..O#...#..#...#...#..#...#...#..#...#...#..#...#...#..
.#....X..#..#....#..#..#....#..#..#....#..#..#....#..#.
In this example, traversing the map using this slope would
cause you to encounter 7 trees.
Starting at the top-left corner of your map and following a
slope of right 3 and down 1, how many trees would you
encounter?
----
This will need more memory than a default Retro build has,
so start by building a version with a much larger memory
footprint and support for 64-bit cells:
make CFLAGS="-DIMAGE_SIZE=8000000 -O2 -DBIT64"
I am doing this in a simple way, constructing a large enough
map that I can just iterate through.
Some math.
My input file has 323 lines with 31 characters per line. With
a horizontal distance of 3 per row, the last position will be
at 969 characters out. So the lines need to be a little over
31 times longer than they are by default.
The easy way to do this is to duplicate the pointer and use
`s:append` to concat them until I get it long enough. Five
iterations is plenty, but I'll do a bit more since I suspect
that I'll need a larger map for the second half.
~~~
:extend (s-s)
#7 [ dup s:append ] times s:keep ;
~~~
We also need to raise the limit on temporary string size.
~~~
#4096 !TempStringMax
~~~
With this, I can create an array for the inputs:
~~~
'input-day-3 'INPUT s:const
(Count_the_number_of_lines)
#0 INPUT [ drop n:inc ] file:for-each-line
(Create_the_array,_and_reserve_space)
'MAP d:create dup , allot
(Fill_in_the_array)
MAP n:inc INPUT [ extend over store n:inc ] file:for-each-line drop
~~~
Now all that's left is to quickly iterate through it, increasing a
counter for the horizontal distance.
~~~
#0 #0 MAP [ over + fetch $# eq? [ &n:inc dip ] if #3 + ] a:for-each
drop n:put nl
~~~
----
# Part 2
Time to check the rest of the slopes - you need to minimize the
probability of a sudden arboreal stop, after all.
Determine the number of trees you would encounter if, for each
of the following slopes, you start at the top-left corner and
traverse the map all the way to the bottom:
Right 1, down 1.
Right 3, down 1. (This is the slope you already checked.)
Right 5, down 1.
Right 7, down 1.
Right 1, down 2.
In the above example, these slopes would find 2, 7, 3, 4, and
2 tree(s) respectively; multiplied together, these produce the
answer 336.
What do you get if you multiply together the number of trees
encountered on each of the listed slopes?
----
This is pretty much the same, except for the last one, which
needs to ignore every other line.
I start by just running through each of the trivial slopes,
reducing down the products.
~~~
'Right var
:calculate
!Right
#0 #0 MAP [ over + fetch $# eq? [ &n:inc dip ] if @Right + ] a:for-each
drop ;
{ #1 #3 #5 #7 } #1 [ calculate * ] a:reduce
~~~
Then create a new MAP, discarding the odd lines.
~~~
{ #0 MAP [ swap dup n:odd? [ nip ] if n:inc ] a:for-each } 'MAP const
~~~
And finally run through the final slope, multiplying by the
other products to get the final answer.
~~~
#0 #0 MAP [ over + fetch $# eq? [ &n:inc dip ] if #1 + ] a:for-each
drop *
n:put nl
~~~

View file

@ -0,0 +1,239 @@
# Day 4: Passport Processing
You arrive at the airport only to realize that you grabbed your
North Pole Credentials instead of your passport. While these
documents are extremely similar, North Pole Credentials aren't
issued by a country and therefore aren't actually valid
documentation for travel in most of the world.
It seems like you're not the only one having problems, though;
a very long line has formed for the automatic passport scanners,
and the delay could upset your travel itinerary.
Due to some questionable network security, you realize you might
be able to solve both of these problems at the same time.
The automatic passport scanners are slow because they're having
trouble detecting which passports have all required fields. The
expected fields are as follows:
byr (Birth Year)
iyr (Issue Year)
eyr (Expiration Year)
hgt (Height)
hcl (Hair Color)
ecl (Eye Color)
pid (Passport ID)
cid (Country ID)
Passport data is validated in batch files (your puzzle input).
Each passport is represented as a sequence of key:value pairs
separated by spaces or newlines. Passports are separated by
blank lines.
Here is an example batch file containing four passports:
ecl:gry pid:860033327 eyr:2020 hcl:#fffffd
byr:1937 iyr:2017 cid:147 hgt:183cm
iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884
hcl:#cfa07d byr:1929
hcl:#ae17e1 iyr:2013
eyr:2024
ecl:brn pid:760753108 byr:1931
hgt:179cm
hcl:#cfa07d eyr:2025 pid:166559648
iyr:2011 ecl:brn hgt:59in
The first passport is valid - all eight fields are present. The
second passport is invalid - it is missing hgt (the Height field).
The third passport is interesting; the only missing field is cid,
so it looks like data from North Pole Credentials, not a passport
at all! Surely, nobody would mind if you made the system
temporarily ignore missing cid fields. Treat this "passport" as
valid.
The fourth passport is missing two fields, cid and byr. Missing
cid is fine, but missing any other field is not, so this passport
is invalid.
According to the above rules, your improved system would report
2 valid passports.
Count the number of valid passports - those that have all
required fields. Treat cid as optional. In your batch file, how
many passports are valid?
----
I first create two data structures: an array of passport
strings, and a buffer to hold the current passport being
read in.
The `Passports` array is constructed using the `buffer:`
words.
~~~
'Passports d:create #1024 allot
'Passport d:create #1024 allot
&Passports n:inc buffer:set
~~~
Importing the passports is pretty easy. There's a blank line after
each entry is complete, so just gather the text until then, then
save the string and record a pointer into the `Passports` listing.
~~~
:add-data '_ s:append &Passport s:append &Passport s:copy ;
:record drop &Passport s:keep buffer:add #0 !Passport ;
:completed? dup s:length n:zero? ;
:finish &Passports n:inc s:length !Passports ;
:passports:load
[ completed? [ record ]
[ add-data ] choose ] file:for-each-line finish ;
'input-day-4 passports:load
~~~
Now that the passport database is loaded, we can simply check
each entry for the required fields, increasing a counter for
each one that proves valid.
~~~
:valid?
[ [ 'byr: s:contains-string? ]
[ 'iyr: s:contains-string? ]
[ 'eyr: s:contains-string? ] tri and and ]
[ [ 'hgt: s:contains-string? ]
[ 'hcl: s:contains-string? ]
[ 'ecl: s:contains-string? ] tri and and ]
[ 'pid: s:contains-string? ] tri and and ;
#0 &Passports [ valid? [ n:inc ] if ] a:for-each
~~~
----
# Part Two
The line is moving more quickly now, but you overhear airport
security talking about how passports with invalid data are
getting through. Better add some data validation, quick!
You can continue to ignore the cid field, but each other
field has strict rules about what values are valid for
automatic validation:
byr (Birth Year) - four digits; at least 1920 and at most 2002.
iyr (Issue Year) - four digits; at least 2010 and at most 2020.
eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
hgt (Height) - a number followed by either cm or in:
If cm, the number must be at least 150 and at most 193.
If in, the number must be at least 59 and at most 76.
hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
pid (Passport ID) - a nine-digit number, including leading zeroes.
cid (Country ID) - ignored, missing or not.
Your job is to count the passports where all required fields
are both present and valid according to the above rules. Here
are some example values:
byr valid: 2002
byr invalid: 2003
hgt valid: 60in
hgt valid: 190cm
hgt invalid: 190in
hgt invalid: 190
hcl valid: #123abc
hcl invalid: #123abz
hcl invalid: 123abc
ecl valid: brn
ecl invalid: wat
pid valid: 000000001
pid invalid: 0123456789
Here are some invalid passports:
... snipped ...
Count the number of valid passports - those that have all
required fields and valid values. Continue to treat cid as
optional. In your batch file, how many passports are valid?
----
I'll start this by defining some things. An array of valid eye
colors, and some words for validating ranges for particular
fields.
~~~
{ 'amb 'blu 'brn 'gry 'hzl 'oth 'grn } 'EYE-COLORS const
:birth-year? s:to-number #1920 #2002 n:between? ;
:issue-year? s:to-number #2010 #2020 n:between? ;
:expire-year? s:to-number #2020 #2030 n:between? ;
:height-imperial? #59 #76 n:between? ;
:height-metric? #150 #193 n:between? ;
~~~
I'll store a count of the number of valid fields in a variable.
~~~
'Valid var
:record-result [ &Valid v:inc ] if ;
~~~
Then I implement the checks for each field.
~~~
:process-byr [ s:length #4 eq? ]
[ birth-year? ] bi and record-result ;
:process-iyr [ s:length #4 eq? ]
[ issue-year? ] bi and record-result ;
:process-eyr [ s:length #4 eq? ]
[ expire-year? ] bi and record-result ;
:process-hgt [ [ c:digit? ] s:filter s:to-number ]
[ #2 s:right 'cm s:eq? ] bi
[ height-metric? ] [ height-imperial? ] choose record-result ;
:process-hcl [ fetch $# eq? ]
[ s:length #7 eq? ]
[ [ 'abcdef0123456789 swap s:contains-char? ] s:filter s:length #6 eq? ] tri
and and record-result ;
:process-ecl EYE-COLORS a:contains-string? record-result ;
:process-pid [ s:length #9 eq? ]
[ [ c:digit? ] s:filter s:length #9 eq? ] bi and record-result ;
~~~
Processing each passport is just a matter of splitting it up
into fields and checking each. The final number of valid
passports will be left on the stack.
~~~
:split s:trim ASCII:SPACE s:tokenize ;
:get-field $: s:tokenize [ ] a:for-each swap ;
:validate-field 'byr [ process-byr ] s:case
'iyr [ process-iyr ] s:case
'eyr [ process-eyr ] s:case
'hgt [ process-hgt ] s:case
'hcl [ process-hcl ] s:case
'ecl [ process-ecl ] s:case
'pid [ process-pid ] s:case
drop-pair ;
:validate #0 !Valid split [ get-field validate-field ] a:for-each ;
#0 &Passports [ dup valid? [ validate @Valid #7 eq? [ n:inc ] if ] [ drop ] choose ] a:for-each
~~~

View file

@ -0,0 +1,145 @@
# Day 5: Binary Boarding
You board your plane only to discover a new problem: you dropped
your boarding pass! You aren't sure which seat is yours, and all
of the flight attendants are busy with the flood of people that
suddenly made it through passport control.
You write a quick program to use your phone's camera to scan all
of the nearby boarding passes (your puzzle input); perhaps you
can find your seat through process of elimination.
Instead of zones or groups, this airline uses binary space
partitioning to seat people. A seat might be specified like
FBFBBFFRLR, where F means "front", B means "back", L means
"left", and R means "right".
The first 7 characters will either be F or B; these specify
exactly one of the 128 rows on the plane (numbered 0 through
127). Each letter tells you which half of a region the given
seat is in. Start with the whole list of rows; the first
letter indicates whether the seat is in the front (0 through
63) or the back (64 through 127). The next letter indicates
which half of that region the seat is in, and so on until
you're left with exactly one row.
For example, consider just the first seven characters of
FBFBBFFRLR:
Start by considering the whole range, rows 0 through 127.
F means to take the lower half, keeping rows 0 through 63.
B means to take the upper half, keeping rows 32 through 63.
F means to take the lower half, keeping rows 32 through 47.
B means to take the upper half, keeping rows 40 through 47.
B keeps rows 44 through 47.
F keeps rows 44 through 45.
The final F keeps the lower of the two, row 44.
The last three characters will be either L or R; these
specify exactly one of the 8 columns of seats on the
plane (numbered 0 through 7). The same process as above
proceeds again, this time with only three steps. L means
to keep the lower half, while R means to keep the upper
half.
For example, consider just the last 3 characters of FBFBBFFRLR:
Start by considering the whole range, columns 0 through 7.
R means to take the upper half, keeping columns 4 through 7.
L means to take the lower half, keeping columns 4 through 5.
The final R keeps the upper of the two, column 5.
So, decoding FBFBBFFRLR reveals that it is the seat at row
44, column 5.
Every seat also has a unique seat ID: multiply the row by 8,
then add the column. In this example, the seat has ID
44 \* 8 + 5 = 357.
Here are some other boarding passes:
BFFFBBFRRR: row 70, column 7, seat ID 567.
FFFBBBFRRR: row 14, column 7, seat ID 119.
BBFFBBFRLL: row 102, column 4, seat ID 820.
As a sanity check, look through your list of boarding passes.
What is the highest seat ID on a boarding pass?
----
This is actually *really* easy. In this case, the boarding passes
ultimately correspond directly to a binary value for the seat id.
So, all that's needed is to convert the boarding pass to a string
representation of the binary value, then convert that to an actual
number.
Since Retro is decmal only, I started by bringing in some code to
work with other bases.
~~~
'Base var
:binary #2 !Base ;
:decimal #10 !Base ;
{{
'0123456789ABCDEF 'DIGITS s:const
'Number var
'Mod var
:convert (c-) &DIGITS swap s:index-of
@Number @Base * + !Number ;
:check-sign (s-s) dup fetch $- eq?
[ #-1 !Mod n:inc ] [ #1 !Mod ] choose ;
---reveal---
:s:to-number<with-base> (s-n)
#0 !Number check-sign [ convert ] s:for-each @Number @Mod * ;
}}
~~~
Then the rest was trivial.
~~~
:c:to-binary
$F [ $0 ] case
$B [ $1 ] case
$L [ $0 ] case
$R [ $1 ] case ;
:s:to-binary-rep [ c:to-binary ] s:map ;
:convert s:to-binary-rep binary s:to-number<with-base> ;
#0 'input-day-5 [ convert n:max ] file:for-each-line
n:put nl
~~~
----
# Part Two ---
Ding! The "fasten seat belt" signs have turned on. Time to find
your seat.
It's a completely full flight, so your seat should be the only
missing boarding pass in your list. However, there's a catch:
some of the seats at the very front and back of the plane don't
exist on this aircraft, so they'll be missing from your list as
well.
Your seat wasn't at the very front or back, though; the seats
with IDs +1 and -1 from yours will be in your list.
What is the ID of your seat?
----
This is also really easy. I just fill in a list of seats to see
which are used, then ignore the empty ones at the start before
returning the first empty one in the middle.
~~~
'Seats d:create #1024 allot
'input-day-5 [ convert &Seats + v:on ] file:for-each-line
&Seats n:inc [ fetch-next n:zero? ] while
[ fetch-next n:zero? ] until &Seats - n:put nl
~~~