We’re only a few days from December, which means it will soon be time for the great programming exercise known as the Advent of Code!
Think of it as an Advent calendar, but chocolates (or cheese, or wine), you’re presented with a new programming puzzle every day between the start of December and Christmas Day, in which you try to save Santa’s mission. You can use whatever programming language you want, and you don’t need to be an expert — as the site says, “just a little programming knowledge and some problem solving skills will get you pretty far.”
Advent of Code started in 2015, and has been taking place every year ever since. The 2020 edition begins on Tuesday, December 1st at 12:00 midnight Eastern time (UTC-5).
Not only do I plan on participating in this year’s Advent of Code, but I might even use a couple of the challenges in the Python class I’m currently teaching on behalf of Computer Coach.
Solving Advent of Code 2019’s day one challenge
Here’s the premise of the 2019 Advent of Code’s challenges: Santa is stuck at the edge of the solar system, and you have to rescue him. Each day’s challenge, which has two parts, gets you closer to bringing him home and saving Christmas.
Day one challenge, part one
Here’s the first part of day one’s challenge:
The Elves quickly load you into a spacecraft and prepare to launch.
At the first Go / No Go poll, every Elf is Go until the Fuel Counter-Upper. They haven’t determined the amount of fuel required yet.
Fuel required to launch a given module is based on its mass. Specifically, to find the fuel required for a module, take its mass, divide by three, round down, and subtract 2.
For example:
- For a mass of
12
, divide by 3 and round down to get4
, then subtract 2 to get2
.- For a mass of
14
, dividing by 3 and rounding down still yields4
, so the fuel required is also2
.- For a mass of
1969
, the fuel required is654
.- For a mass of
100756
, the fuel required is33583
.The Fuel Counter-Upper needs to know the total fuel requirement. To find it, individually calculate the fuel needed for the mass of each module (your puzzle input), then add together all the fuel values.
What is the sum of the fuel requirements for all of the modules on your spacecraft?
While the problems in the Advent of Code are the same for every participant, the data for each participant is different (there’s a sign-up process, which gives you an account, your own progress tracker, and your own data). This prevents participants from simply sharing the solution.
Here are the module masses that were provided for my account:
134492 88713 84405 148193 95951 63545 137840 65558 124836 95431 77622 91864 108677 116871 119496 97172 86115 105704 68613 77114 114013 52766 57048 80814 73888 58253 135934 97409 112439 98262 116047 57456 124261 83006 101495 133449 111372 56146 87818 92209 149259 124559 141838 147988 65703 125566 59650 139564 92430 126307 120406 147383 84362 51529 146366 131840 53270 71886 118767 104311 126181 76964 129430 95489 91098 54133 110057 107276 118226 96104 135382 85152 61697 143417 148879 126846 130205 111170 86687 113729 123330 56976 148470 66028 129715 75686 74964 148258 72669 88809 78173 92699 124806 67217 139066 136002 135730 145708 142054 135772
I decided to use the Python REPL to tackle this problem.
My first step was to copy the numbers above, paste them into a triple-quoted string, and assign that string to the variable raw_input
:
raw_input = """134492 88713 84405 148193 95951 63545 137840 65558 124836 95431 77622 91864 108677 116871 119496 97172 86115 105704 68613 77114 114013 52766 57048 80814 73888 58253 135934 97409 112439 98262 116047 57456 124261 83006 101495 133449 111372 56146 87818 92209 149259 124559 141838 147988 65703 125566 59650 139564 92430 126307 120406 147383 84362 51529 146366 131840 53270 71886 118767 104311 126181 76964 129430 95489 91098 54133 110057 107276 118226 96104 135382 85152 61697 143417 148879 126846 130205 111170 86687 113729 123330 56976 148470 66028 129715 75686 74964 148258 72669 88809 78173 92699 124806 67217 139066 136002 135730 145708 142054 135772"""
Now that I had the data in a string, I could split the string into an array, using the newline character as the delimiter. I named the array split_input
:
>>> split_input = raw_input.split("\n") >>> split_input ['134492', '88713', '84405', '148193', '95951', '63545', '137840', '65558', '124836', '95431', '77622', '91864', '108677', '116871', '119496', '97172', '86115', '105704', '68613', '77114', '114013', '52766', '57048', '80814', '73888', '58253', '135934', '97409', '112439', '98262', '116047', '57456', '124261', '83006', '101495', '133449', '111372', '56146', '87818', '92209', '149259', '124559', '141838', '147988', '65703', '125566', '59650', '139564', '92430', '126307', '120406', '147383', '84362', '51529', '146366', '131840', '53270', '71886', '118767', '104311', '126181', '76964', '129430', '95489', '91098', '54133', '110057', '107276', '118226', '96104', '135382', '85152', '61697', '143417', '148879', '126846', '130205', '111170', '86687', '113729', '123330', '56976', '148470', '66028', '129715', '75686', '74964', '148258', '72669', '88809', '78173', '92699', '124806', '67217', '139066', '136002', '135730', '145708', '142054', '135772']
split_input
is an array of strings which needed to be converted into integer values.
In many other languages, I’d do this by using the map
function to apply a “convert a string to its integer value” function to every item in the array, creating a resulting array called masses
. Here’s the Python version of that approach:
>>> masses = list(map(int, split_input)) >>> masses [134492, 88713, 84405, 148193, 95951, 63545, 137840, 65558, 124836, 95431, 77622, 91864, 108677, 116871, 119496, 97172, 86115, 105704, 68613, 77114, 114013, 52766, 57048, 80814, 73888, 58253, 135934, 97409, 112439, 98262, 116047, 57456, 124261, 83006, 101495, 133449, 111372, 56146, 87818, 92209, 149259, 124559, 141838, 147988, 65703, 125566, 59650, 139564, 92430, 126307, 120406, 147383, 84362, 51529, 146366, 131840, 53270, 71886, 118767, 104311, 126181, 76964, 129430, 95489, 91098, 54133, 110057, 107276, 118226, 96104, 135382, 85152, 61697, 143417, 148879, 126846, 130205, 111170, 86687, 113729, 123330, 56976, 148470, 66028, 129715, 75686, 74964, 148258, 72669, 88809, 78173, 92699, 124806, 67217, 139066, 136002, 135730, 145708, 142054, 135772]
It works, but from a Python programming point of view, it just doesn’t feel right.
The Pythonic approach would involve using a list comprehension instead of map
(and then using the resulting iterator into a list). It just seems more readable:
>>> masses = [int(string) for string in split_input] >>> masses [134492, 88713, 84405, 148193, 95951, 63545, 137840, 65558, 124836, 95431, 77622, 91864, 108677, 116871, 119496, 97172, 86115, 105704, 68613, 77114, 114013, 52766, 57048, 80814, 73888, 58253, 135934, 97409, 112439, 98262, 116047, 57456, 124261, 83006, 101495, 133449, 111372, 56146, 87818, 92209, 149259, 124559, 141838, 147988, 65703, 125566, 59650, 139564, 92430, 126307, 120406, 147383, 84362, 51529, 146366, 131840, 53270, 71886, 118767, 104311, 126181, 76964, 129430, 95489, 91098, 54133, 110057, 107276, 118226, 96104, 135382, 85152, 61697, 143417, 148879, 126846, 130205, 111170, 86687, 113729, 123330, 56976, 148470, 66028, 129715, 75686, 74964, 148258, 72669, 88809, 78173, 92699, 124806, 67217, 139066, 136002, 135730, 145708, 142054, 135772]
Once I had the masses, I could then calculate the fuel requirements for each mass. This may be the only time I’ve ever made use of Python’s floor division operator, which performs an integer division, rounding down:
>>> fuel_requirements = list(map(lambda mass: mass // 3 - 2, masses)) >>> fuel_requirements [44828, 29569, 28133, 49395, 31981, 21179, 45944, 21850, 41610, 31808, 25872, 30619, 36223, 38955, 39830, 32388, 28703, 35232, 22869, 25702, 38002, 17586, 19014, 26936, 24627, 19415, 45309, 32467, 37477, 32752, 38680, 19150, 41418, 27666, 33829, 44481, 37122, 18713, 29270, 30734, 49751, 41517, 47277, 49327, 21899, 41853, 19881, 46519, 30808, 42100, 40133, 49125, 28118, 17174, 48786, 43944, 17754, 23960, 39587, 34768, 42058, 25652, 43141, 31827, 30364, 18042, 36683, 35756, 39406, 32032, 45125, 28382, 20563, 47803, 49624, 42280, 43399, 37054, 28893, 37907, 41108, 18990, 49488, 22007, 43236, 25226, 24986, 49417, 24221, 29601, 26055, 30897, 41600, 22403, 46353, 45332, 45241, 48567, 47349, 45255]
The map/list/lambda approach works just fine, but once again, a list comprehension just seems more elegant to my eye:
>>> fuel_requirements = [mass // 3 - 2 for mass in masses] >>> fuel_requirements [44828, 29569, 28133, 49395, 31981, 21179, 45944, 21850, 41610, 31808, 25872, 30619, 36223, 38955, 39830, 32388, 28703, 35232, 22869, 25702, 38002, 17586, 19014, 26936, 24627, 19415, 45309, 32467, 37477, 32752, 38680, 19150, 41418, 27666, 33829, 44481, 37122, 18713, 29270, 30734, 49751, 41517, 47277, 49327, 21899, 41853, 19881, 46519, 30808, 42100, 40133, 49125, 28118, 17174, 48786, 43944, 17754, 23960, 39587, 34768, 42058, 25652, 43141, 31827, 30364, 18042, 36683, 35756, 39406, 32032, 45125, 28382, 20563, 47803, 49624, 42280, 43399, 37054, 28893, 37907, 41108, 18990, 49488, 22007, 43236, 25226, 24986, 49417, 24221, 29601, 26055, 30897, 41600, 22403, 46353, 45332, 45241, 48567, 47349, 45255]
And now that I had the fuel requirements for each module, all I had to do was get their sum:
>>> total_fuel = sum(fuel_requirements) >>> total_fuel 3454942
I entered the value for total_fuel into the solution text field, and Advent of Code immediately told me that I had solved part one of the day one challenge! So far, so good.
Day one challenge, part two
Part two of the challenge was a refinement of part one:
During the second Go / No Go poll, the Elf in charge of the Rocket Equation Double-Checker stops the launch sequence. Apparently, you forgot to include additional fuel for the fuel you just added.
Fuel itself requires fuel just like a module – take its mass, divide by three, round down, and subtract 2. However, that fuel also requires fuel, and that fuel requires fuel, and so on. Any mass that would require negative fuel should instead be treated as if it requires zero fuel; the remaining mass, if any, is instead handled by wishing really hard, which has no mass and is outside the scope of this calculation.
So, for each module mass, calculate its fuel and add it to the total. Then, treat the fuel amount you just calculated as the input mass and repeat the process, continuing until a fuel requirement is zero or negative. For example:
- A module of mass
14
requires2
fuel. This fuel requires no further fuel (2 divided by 3 and rounded down is0
, which would call for a negative fuel), so the total fuel required is still just2
.- At first, a module of mass
1969
requires654
fuel. Then, this fuel requires216
more fuel (654 / 3 - 2
).216
then requires70
more fuel, which requires21
fuel, which requires5
fuel, which requires no further fuel. So, the total fuel required for a module of mass1969
is654 + 216 + 70 + 21 + 5 = 966
.- The fuel required by a module of mass
100756
and its fuel is:33583 + 11192 + 3728 + 1240 + 411 + 135 + 43 + 12 + 2 = 50346
.What is the sum of the fuel requirements for all of the modules on your spacecraft when also taking into account the mass of the added fuel? (Calculate the fuel requirements for each module separately, then add them all up at the end.)
Upon reading this, my first thought was:
The trick to writing recursive functions is to solve the “escape” case first — that is, the case where you stop the recursion and just return a value.
For this problem, the “escape” case is when the repeated fuel calculation gives a result of 0 or less:
if result <= 0: return 0
Otherwise, take the result, and apply the fuel calculation to it again. That’s what gives us the recursive part. Here’s the resulting if
statement:
if result <= 0: return 0 else: return result + fuel_required(result)
And finally, we have to handle the initial calculation. The end result is the fuel_required
function:
def fuel_required(mass): result = mass // 3 - 2 if result <= 0: return 0 else: return result + fuel_required(result)
Now that we have the fuel_required
function, we can apply it to every item in the masses
array from part one:
>>> updated_fuel_requirements = [fuel_required(mass) for mass in masses] >>> updated_fuel_requirements [67212, 44325, 42171, 74062, 47941, 31741, 68888, 32746, 62387, 47682, 38780, 45902, 54306, 58402, 59715, 48553, 43027, 52822, 34277, 38525, 56974, 26354, 28495, 40375, 36913, 29094, 67934, 48670, 56187, 49098, 57991, 28698, 62098, 41470, 50716, 66693, 55654, 28043, 43877, 46073, 74596, 62245, 70886, 73962, 32822, 62751, 29795, 69750, 46184, 63121, 60171, 73658, 42148, 25734, 73150, 65887, 26606, 35910, 59351, 52123, 63058, 38449, 64681, 47710, 45516, 27037, 54994, 53606, 59081, 48018, 67657, 42543, 30817, 71674, 74407, 63393, 65068, 55551, 43311, 56833, 61632, 28459, 74203, 32983, 64824, 37810, 37450, 74096, 36304, 44373, 39054, 46318, 62371, 33576, 69500, 67968, 67832, 72820, 70993, 67853]
This yields the updated fuel requirements for each module. The final step was to get their sum:
>>> updated_total_fuel = sum(updated_fuel_requirements) >>> updated_total_fuel 5179544
Entering the value of updated_total_fuel
into the solution text field completed the day one challenge.
One reply on “The Advent of Code is coming in a few days!”
[…] I wrote in the previous post, the Advent of Code is happening soon — it start on Tuesday, December 1st and runs all the way to […]