Advent of code 2022
That time of the year again! I’ve always wanted to do one of these to the end. I don’t guarantee I’ll finish it, but in the meantime, I’ll post here the solutions I come up with for each day, along with some commentaries on how I approached each puzzle.
Here’s the list of days I’ve completed so far, in case this page gets too long to comfortably navigate:
Currently I’m ~8 days behind. I’ll probably try to give a go to the rest of problems, but later this week.
Day 1
The input looks like:
100
200
300
400
500
600
The problem is doing a sum of the numbers, grouped by the new line separations (so, here, 100+200+300, 400, 500+600). Then comes the easy part: you have to get the highest number for part one, then the sum of the top three for part two of the puzzle.
First I thought I could do it in Python
, picking all items per row and appending them to a new list until it reaches an empty line, so that you get something easy to sum. The result to sum would be something like:
[100,200,300]
[400]
[500,600]
But then I thought, what the hell, doing it in a Bash oneliner is much more entertaining. I didn’t want to write a full script for what felt like a menial task. My first thought was to divide the output in columns - maybe something like:
100,400,500
200,,600
300,,
This would be as easy as a sum of all the elements of each column in awk
, but it was a bit of a pain to produce with paste
or such. Then I realized I just had to transform the new lines into pluses (+
), replace any consecutive pluses (++
) with new lines (effectively grouping the input) and feed to a calculator (I have a sweet spot for bc
). Essentially, I wanted to produce this:
100+200+300
400
500+600
My solution is as simple as tr
+sed
for some basic character substitutions. From there it’s a matter of sort
ing and extracting the first item (head
) for the requirements of the first half of the puzzle:
tr '\n' '+' < input.txt | sed 's/++/\n/g' | bc | sort -rn | head -1
To get the sum of the top 3, I just modified head
and added an awk
column sum:
tr '\n' '+' < input.txt | sed 's/++/\n/g' | bc | sort -rn | head -3 | awk '{sum += $1} END {print sum}
And that’s it!
Day 2
The input: {A, B, C}
is the opponent’s choice and {X, Y, Z}
is yours in rock
, paper
and scissors
games. It looks like:
A X
B Y
C Z
The problem is calculating a total winning score, where you are given points for the choice (column 2, where rock=X=1
, paper=Y=2
, scissors=Z=3
) and for winning (6 points) or getting a draw (3 points).
At this point there are several approaches - rock, paper & scissors
is a common beginner programmer project. I chose to transform both columns to ordinals, then map the difference between columns to win/lose conditions. I used a bit too much sed
for my taste, and a 3x3 choice matrix solution would have been quicker, but I really wanted to make this in bash
again (shame on me!). I also learned a couple dirty tricks of awk
derivatives.
First, the ascii values
: Since all are capital letters, an easy way to get alphabet ordinals would be to convert the first column to ASCII values (values 65 onwards for uppercase letters):
echo $(($(printf '%d' \'A ) -64))
> 1
Turns out regular awk
doesn’t have a handy implementation for this… but gwak
does as ord()
! so you can do this:
gawk -l ordchr '{print $1" = "ord($1)-65,
$2 " = " ord($2)-88}' example_input.txt
> A = 0 X = 0
> B = 1 Y = 1
> C = 2 Z = 2
Now it’s a matter of checking the difference, and adjusting the resulting numbers to the win-lose choice matrix.
My final solution for part 1 of this day ended up like this:
choice_points=$(gawk -l ordchr '
{s+=ord($2)-87} END
{print s}' input.txt)
victory_points=$(gawk -l ordchr '{
col1=ord($1)-64;
col2=ord($2)-88;
print (col1-col2)}' input.txt |
sed 's/0/6/g ; s/3/6/g ; s/-1/0/g ; s/1/3/g ; s/2/0/g' |
awk '{sum += $1} END {print sum}')
echo $choice_points+$victory_points | bc
The second variable transforms the letters to ordinals (1-3 for the A-C, 0-2 for the X-Z in this case), so that wins(6 points)=[0|3]
, draws(3 points)=[1]
and loses(0 points)=[-1|2]
in the difference between both columns. For example, (elf) ROCK VS PAPER (you)
would be here 1-1 = 0 = you win
. Then, it’s a matter of summing columns and scores. Note that the order of the substitutions matters.
The second part of the puzzle reveals that {Y,X,Z}
actually stood for the desired victory condition! Effectively, the same problem, but departing from the result rather than the choices of you and your opponent.
Getting the victory points has few shenanigans:
victory_points=$(gawk -l ordchr '
{col2=ord($2)-88; score=col2*3}
{sum+=score} END {print sum}' input.txt)
However, guessing the combinations needed is where I’ll pay my dirty sed
pipping above. After some fiddling I ended up dividing between more predictable draws (where you just have to filter the input for draws then pick the numeric value of column 1):
draw_choice_points=$(gawk -l ordchr '
{col1=ord($1)-64; col2=ord($2)-87}
{if (col2==2) print $1, col1}' input.txt |
sort | uniq -c |
awk '{col1=$1*$3; sum+=col1} END {print sum}')
And the more lousy win and lose conditions. At this point I gave up with the ASCII approach and I just initialized an array detailing the conditions and applied it to the thing:
rest_choice_points=$(gawk -l ordchr 'BEGIN {
arr["A X"] = 3
arr["B Z"] = 3
arr["C X"] = 2
arr["A Z"] = 2
arr["B X"] = 1
arr["C Z"] = 1
}{print arr[$0]}' input.txt | sed '/^[[:blank:]]*$/ d' | sort | uniq -c | awk '{res=$1*$2; sum+=res} END {print sum}')
Then the final sum (I told you I like bc
!):
echo $victory_points+$draw_choice_points+$rest_choice_points | bc
And that’s it. By the second part it got a bit out of hand with awk
(who would have thought that a language released 45 years ago feels awkward?) but it was fun!
Day 3
The input looks like this:
vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw
The problem is dividing the string evenly, then searching for common characters, then doing a sum of points (1-26 for lowercase letters, 27-52 for uppercase).
This one was easy, I took awk
and… nah, kidding, I used python
, like a sane person would. Here’s my solution for part 1:
# read input, clean
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
# divide in two sacks (ns1, ns2), find common characters, append to a list
common_chars = []
for i in input:
ns1= i[0:len(i)//2]
ns2= i[len(i)//2:len(i)]
common = list(set(ns1)&set(ns2))
common_chars.append(common[0])
# some ASCII conversion and some adjusting for uppercase
priorities = [ord(l.swapcase())-64 for l in common_chars]
priorities = [d-6 if d>26 else d for d in priorities]
print(sum(priorities))
(PS: I was playing around with ChatGPT’s open beta and fed it the for
loop above to improve it, and after some gentle prompting it replied with the following working oneliner that doesn’t use sets:)
common_chars = [next((char for char in i[0:len(i)//2] if char in i[len(i)//2:len(i)]), "") for i in input]
Anyway: part two of the puzzle swaps the problem from row based to column-based (in groups of three). So:
# generates list of lists in chunks of 3
# then finds common characters between each of the 3 lists
grouped = [input[i:i+3] for i in range(0, len(input), 3)]
common_chars = []
for i in grouped:
common_chars.append(list(set(i[0]) & set(i[1]) & set(i[2]))[0])
# same as before
priorities = [ord(l.swapcase())-64 for l in common_chars]
priorities = [d-6 if d>26 else d for d in priorities]
print(sum(priorities))
And that’s it!
Day 4
The input looks like this:
2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8
The problem is finding those ranges that fully contain others.
I really didn’t want to keep using awk
, but in the end this was a problem too easy to solve with it. Took me about three minutes to solve part 1:
sed 's/-/,/g' example_input.txt # changes - by , so that it can be treated as a column
| awk 'BEGIN {FS=","};
{if (($1<=$3 && $2>=$4) ||
($1>=$3 && $2<=$4))
print $1"-"$2,$3"-"$4}' | wc -l
# if statement: controls if range 1 is covered by range 2
# or viceversa
# then some unnecesary pretty printing, and counts output lines
The second part requires you get all ranges that overlap in any way. I found it quicker to calculate how many don’t overlap at all, then rest that number to the total number of lines:
# same as above, but conditions control ranges don't overlap
non_overlapping=$(sed 's/-/,/g' input.txt |
awk ' BEGIN {FS=","};
{if (($1<$3) && ($2<$3) ||
($1>$4) && ($2>$4))
print $1"-"$2,$3"-"$4}' | wc -l)
all=$(wc -l < input.txt)
echo $all-$non_overlapping | bc
And that’s it!
Day 5
The input looks like:
[D]
[N] [C]
[Z] [M] [P]
1 2 3
move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2
The problem is moving the [X]
crates in column-based stacks according to the instructions below. A problem of stacks, basically.
First, we divide the input in two big lines: crates and instructions. Luckily, we can just divide at the empty line:
with open("ex_input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
# separates by empty line in two lists: crates, and move instructions
crates_instructions = [input[:input.index('')], input[input.index('')+1:]]
Getting the “columns” right for the crates input required a bit of tinkering, but ultimately it was simple. Basically I wanted this:
[['[]' '[D]' '[]']
['[N]' '[C]' '[]']
['[Z]' '[M]' '[P]']]
so that I could get this kind of input, where I could just use python’s pop()
to follow the instructions:
[['[Z]', '[N]'],
['[M]', '[C]', '[D]',
['[P]']]
In order to do that I used the following code:
# Create empty crates ("[]")
crates = [re.sub(" ", " [] ", row).strip() for row in crates]
# split by spaces, remove the last line with the numbers as I don't really need it
crates = [crate.split() for crate in crates][:-1]
# to array, for convenience:
crate_array = np.array(crates)
# transpose coordinates, reverse items in each row, end with a list of lists of different lengths
crate_array = crate_array.T
crate_array = np.flip(crate_array, axis= 1)
crates = crate_array.tolist()
# remove empty crates
# (I only needed them to flip the array comfortably)
for list_crates in crates:
list_crates[:] = [item for item in list_crates if item != "[]"]
Then I just need to follow instructions, remove the number of items required and append it to the required stack:
instructions = crates_instructions[1]
for instruction in instructions:
# takes instruction lines and picks relevant numbers
inst_list = instruction.split(" ")
ncrates = int(inst_list[1])
origin = int(inst_list[3])-1 # -1 because it'll be treated as an index
destiny = int(inst_list[5])-1 # idem
# for each required item to move
for i in range(1,ncrates+1):
# it pops it from the required stack
moving_crate = crates[origin].pop()
# and appends it elsewhere:
crates[destiny].append(moving_crate)
# the final output is just joining all the last items of each list in a string:
print(''.join([item[-1].strip('[]') for item in crates]))
Verbose, but not really hard once you figure out the input formatting.
Part two of the puzzle requires the same output, but keeping the order of the removed stack.
I modified the instructions block slightly to adjust to this, working with list slices rather than pop()
:
for instruction in instructions:
#same as above:
inst_list = instruction.split(" ")
ncrates = int(inst_list[1])
origin = int(inst_list[3])-1
destiny = int(inst_list[5])-1
moving_crates = crates[origin][-ncrates:]
crates[origin] = crates[origin][:-ncrates]
for single_crate in moving_crates:
crates[destiny].append(single_crate)
Aaand that’s it!
Day 6
The input looks like:
mjqjpqmgbljsphdztnvjfqwrcgsmlb
The problem is identifying the index of the first group where four unique characters happen (actually, index+4, since m
, mj
, mjq
in the example count towards the number).
My solution is simple: transform each group of four characters to sets (to get unique characters). Control for the length of that set, and move on if it’s less than 4 unique characters.
s,e = 0, 4
marker = set(input[0][s:e])
while len(marker) < 4:
s+=1
e+=1
marker = set(input[0][s:e])
print(s+4)
You could make it more terse, probably.
The second part requires that the substring become 14 unique characters. Easy to modify - just change the 4s for 14’s:
s,e = 0, 14
marker = set(input[0][s:e])
while len(marker) < 14:
s+=1
e+=1
marker = set(input[0][s:e])
print(s+14)
And that’s it!
Day 7
Day 7
The input looks like this:
$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k
The problem is parsing this file structure and doing a sum per directory of file sizes, then outputting… well, actually, the problem is, errr… implementing a tree filesystem. Ugh.
This one was a bit more complicated. My first instinct was that I needed something like this:
[{'/': ['a', 14848514, 8504156, 'd']}
{'a': ['e', 29116, 2557, 62596]}
{'e': [584]}
{'d': [4060174, 8033020, 5626152, 7214296]}]
Where I could apply the following process:
- Sum all directories where all elements are integers (here, e and d)
- Substitute the names of the directories elsewhere for the sum
- Repeat untill all directories are a single integer
Not terrible, but what I actually needed what this:
[{'/': ['a', 14848514, 8504156, 'd'],
'a': ['e', 29116, 2557, 62596]
'e': [584],
'd': [4060174, 8033020, 5626152, 7214296]}]
Mostly because working with lists withing dictionaries within lists was a road to madness. Working like this is way cleaner. Also!! The input example doesn’t match the actual input! That was a lot of fun. So rather, since the same name can appear in different places, you want:
[{'/': ['a', 14848514, 8504156, 'd'],
'/a': ['e', 29116, 2557, 62596]
'/a/e': [584],
'/d': [4060174, 8033020, 5626152, 7214296]}]
After quite some effort I ended up doing this for part one (comments directly on the code):
import re
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
tree = []
pwd = "/"
for line in input:
if line.startswith("$ cd"):
ls = []
match = re.search("cd (.*)$", line)
cd_dir = match.group(1)
# store first ls
if cd_dir == "/":
# strip if dir
dir = re.search(r"dir (.*)", line)
file_size = re.search(r"\d+", line)
if dir:
ls.append(pwd+dir.group(1))
if file_size:
ls.append(int(file_size.group(0)))
tree.append({"/" : ls})
if cd_dir != "..":
if cd_dir != "/":
pwd = pwd+cd_dir+"/"
tree.append({pwd : ls})
if cd_dir == "..":
pwd = pwd.split("/")
pwd = [x for x in pwd if x != ""]
pwd = pwd[:-1]
pwd = "/"+"/".join(pwd)+"/"
pwd = pwd.replace("//","/")
if "$" not in line:
# get dir name if dir
dir = re.search(r"dir (.*)", line)
if dir:
ls.append(pwd+dir.group(1)+"/")
# get file sizes as int
file_size = re.search(r"\d+", line)
if file_size:
ls.append(int(file_size.group(0)))
# transform list of dictionaries into a megadictionary
# with all paths
flat_tree = {}
for d in tree:
flat_tree.update(d)
After getting the dictionary of dictionaries, it was a matter of replacing the values:
def sum_values(tree):
for key, values in tree.items():
# control for done sums
if isinstance(values, int):
continue
# if its a list of digits, sum
if all(isinstance(x, int) for x in values):
total = sum(values)
tree.update({key:total})
return tree
def replace_values(tree):
tree = sum_values(tree)
keys = list(tree.keys())
for key, values in tree.items():
if isinstance(values, list):
to_check = [x for x in values if isinstance(x, str)]
for directory in to_check:
if isinstance(tree[directory], int):
values[values.index(directory)] = tree[directory]
tree[key] = values
# recursive call:
all_values = list(tree.values())
for i in all_values:
if not isinstance(i, int):
replace_values(tree)
tree = sum_values(tree)
return tree
else:
return tree
# Run the above code, and filter
result_size = replace_values(flat_tree)
final = {k: v for k, v in result_size.items() if v < 100000}
print("Solution part 1:", sum(final.values()))
Part two requires that you calculate left space in the filesystem, get how much space you need for some update and pick the smallest folder that fulfills that criteria:
# part two
left_space = 70000000-result_size.get("/")
to_free = 30000000-left_space
final2 = {k: v for k, v in result_size.items() if v > to_free}
candidates = list(final2.values())
print("Solution part 2:min(candidates))
Way easier once you have implemented the filesystem! Pew! And that’s it!
Day 8
The input looks like this:
30373
25512
65332
33549
35390
The problem is (succintely) seeing if numbers up, down, left and right of a given tree are smaller than the tree at a given position, and then marking that position and counting a total of marks.
My solution for part one was approaching this as boolean mask, where I just needed to transform the orientation of the input array to adjust to the orientation (from up, from down, from left and from right), then apply a single function that checks for each element if the elements that precede it in the row are smaller.
The input array and function to check it from a certain orientation looked like this:
import numpy as np
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
digits_input = [list(x) for x in input]
input_array = np.array(digits_input, dtype=int)
def check_array(input_array):
mask = []
for row in input_array:
row_mask = []
for i in range(len(row)):
if i == 0:
row_mask.append(True)
else:
posterior = row[:i]
if all(row[i] > x for x in posterior):
row_mask.append(True)
else:
row_mask.append(False*(len(row)-i-1))
mask.append(row_mask)
mask = np.array(mask,dtype=bool)
return mask
And the actual transformations for the array to be checked like this:
# set true condition for all elements in edges
edge_mask = np.zeros_like(input_array, dtype=bool)
edge_mask[0] = True
edge_mask[-1] = True
edge_mask[:,0] = True
edge_mask[:,-1] = True
# left to right
lr_mask = check_array(input_array)
# right to left
rl_array = np.flip(input_array, axis = 1)
rl_mask = check_array(rl_array)
rl_mask = np.flip(rl_mask, axis = 1)
# up to down
ud_array = input_array.T
ud_mask = check_array(ud_array)
ud_mask = ud_mask.T
# down to up
du_array = np.flip(input_array.T, axis = 1)
du_mask = check_array(du_array)
du_mask = np.flip(du_mask.T, axis = 0)
# merge arrays:
mask = np.logical_or(lr_mask, edge_mask)
mask = np.logical_or(mask, rl_mask)
mask = np.logical_or(mask, lr_mask)
mask = np.logical_or(mask, ud_mask)
mask = np.logical_or(mask, du_mask)
print("solution 1:", mask.sum())
Not too hard, actually - took me an embarrasing amount of time to come up with this solution, as I was making a function for each orientation and it took a bit of rethinking to make it less complicated.
Part 2 requires that you calculate a scenic score by multiplying the number of numbers that are set to True
in the mask: you have to get the highest score. After some fiddling, it ended up like this:
def check_conseqs(tree, list_input):
"""
checks how many of the trees preceding, following, up or down
are bigger than a given tree
"""
smaller_than_input = []
# control for edge
if len(list_input) == 0:
return 1
for adj_tree in list_input:
if tree > adj_tree:
smaller_than_input.append(tree)
else:
return len(smaller_than_input)+1
return len(smaller_than_input)
def check_score(arr):
"""
iterates over all trees and calculates the score
"""
grid_score = []
for row in range(len(arr)):
row_score = []
for tree in range(len(arr[row])):
following = arr[row][tree:][1:]
score = check_conseqs(arr[row][tree], following)
row_score.append(score)
grid_score.append(row_score)
scores = np.array(grid_score)
return scores
#looking right
lr_scenery = check_score(input_array)
# looking left
rl_scenery = np.flip(check_score(rl_array), axis = 1)
# looking down
ud_scenery = check_score(ud_array)
ud_scenery = ud_scenery.T
# looking up
du_scenery = check_score(du_array)
du_scenery = np.flip(du_scenery.T, axis = 0)
# sets edges at 0...
edge_mask = np.ones(input_array.shape, dtype = np.int8)
edge_mask[0] = 0
edge_mask[-1] = 0
edge_mask[:,0] = 0
edge_mask[:,-1] = 0
final_scenery_score = du_scenery*rl_scenery*ud_scenery*lr_scenery*edge_mask
print("solution 2:", np.max(final_scenery_score))
Very similar approach (one function for all calculations, just rotating the input, iterating over rows to create a new array with given scores). I’m sure there was a more efficient way to do this, but well, I’m ok with the “naive” solution.
Day 9
The input looks like a set of instructions to move two elements (head
and tail
) around a board:
R 4
U 4
L 3
D 1
R 4
D 1
L 5
R 2
The problem is that two elements (head and tail) will be moving, one following instructions and the other tracking it closely.
My solution was to create two classes (one for each element) and just have them move according to the other. I’m refreshed some of my knowledge of classes in Python and feel the final solution is fairly elegant, for once in the last days!
import numpy as np
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
class head:
def __init__(self):
self._v = 0
self._h = 0
def coords(self):
return self._v, self._h
def move(self, where):
if where == "R":
self._h +=1
if where == "L":
self._h -=1
if where == "U":
self._v +=1
if where == "D":
self._v -=1
return head.coords()
class tail(head):
def follow_head(self, head_coords, prev_position = None):
diffv = head_coords[0]-self.coords()[0]
diffh = head_coords[1]-self.coords()[1]
# in case of diagonal moving needed
if abs(diffh) == 2 and abs(diffv) == 1 or \
abs(diffh) == 1 and abs(diffv) == 2:
# match head previous position
self._v = prev_position[0]
self._h = prev_position[1]
return self.coords()
# regular moving:
if diffh > 1:
self.move("R")
if diffh < -1:
self.move("L")
if diffv > 1:
self.move("U")
if diffv < -1:
self.move("D")
return self.coords()
head = head()
tail = tail()
record_steps = []
for inst in input:
inst = inst.split(" ")
for _ in range(int(inst[1])):
prev_position = head.coords()
head.move(inst[0])
steps = tail.follow_head(head.coords(), prev_position)
record_steps.append(steps)
# how many unique steps have been taken by tail?
print("Solution 1:",len(set(record_steps))
Part 2 involves adding 10 objects to the grid (sigh). I’m glad I approached this as a class problem, but I didn’t model diagonal movement properly. So much for elegance. What I did is retouch the follwing formula to include diagonal movement cases and track each previous knot’s position (since the cheat of just moving to the last head’s position wasn’t enough now):
import numpy as np
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
class head:
def __init__(self):
self._v = 0
self._h = 0
def coords(self):
return self._v, self._h
def move(self, where, how=None):
if where == "R":
self._h +=1
if where == "L":
self._h -=1
if where == "U":
self._v +=1
if where == "D":
self._v -=1
# diagonal:
if where == "UR":
self._h +=1
self._v +=1
if where == "DR":
self._h +=1
self._v -=1
if where == "UL":
self._h -=1
self._v +=1
if where == "DL":
self._h -=1
self._v -=1
return self.coords()
class tail(head):
def follow_head(self, headcoords, prev_position = None):
diffv = headcoords[0]-self.coords()[0]
diffh = headcoords[1]-self.coords()[1]
#print(diffv, diffh)
if diffh == 0 or diffv == 0:
if diffh > 1:
self.move("R")
if diffh < -1:
self.move("L")
if diffv > 1:
self.move("U")
if diffv < -1:
self.move("D")
else:
if (diffh == 2 and diffv == 1) or (diffh == 1 and diffv == 2):
self.move("UR")
if (diffh == 2 and diffv == -1) or (diffh == 1 and diffv == -2):
self.move("DR")
if (diffh == -2 and diffv == 1) or (diffh == -1 and diffv == 2):
self.move("UL")
if (diffh == -2 and diffv == -1) or (diffh == -1 and diffv == -2):
self.move("DL")
if diffh == 2 and diffv == 2:
self.move("UR")
if diffh == -2 and diffv == 2:
self.move("UL")
if diffh == 2 and diffv == -2:
self.move("DR")
if diffh == -2 and diffv == -2:
self.move("DL")
return self.coords()
part1_head = head()
part1_tail = tail()
record_steps = []
for inst in input:
inst = inst.split(" ")
for _ in range(int(inst[1])):
prev_position = part1_head.coords()
part1_head.move(inst[0])
steps = part1_tail.follow_head(part1_head.coords(), prev_position)
record_steps.append(steps)
print("Solution 1:",len(set(record_steps)))
print("")
part2_head = head()
tail1 = tail()
tail2 = tail()
tail3 = tail()
tail4 = tail()
tail5 = tail()
tail6 = tail()
tail7 = tail()
tail8 = tail()
tail9 = tail()
record_steps = []
for inst in input:
inst = inst.split(" ")
for _ in range(int(inst[1])):
part2_head.move(inst[0])
tail1.follow_head(part2_head.coords())
tail2.follow_head(tail1.coords())
tail3.follow_head(tail2.coords())
tail4.follow_head(tail3.coords())
tail5.follow_head(tail4.coords())
tail6.follow_head(tail5.coords())
tail7.follow_head(tail6.coords())
tail8.follow_head(tail7.coords())
steps = tail9.follow_head(tail8.coords())
record_steps.append(steps)
print("Solution 2:",len(set(record_steps)))
Aaand that’s it! It’s getting harder! This one took me a bit more of thinking, and I wish I had the time to rewrite it from scratch knowing the second part - I probably would have approached this differently.
Day 10
The input looks like this:
noop
addx 3
addx -5
The problem is computing cycles according to instructions (1 per line, +1 for each addx
) and checking a value (X
) at certain cycles.
My solution to the first part was a bit trivial, and made me fear the worst for part 2:
import re
with open("input.txt") as file:
input = file.readlines()
input = [line.strip('\n') for line in input]
X = 1
cycle = 1
cycles = [20, 60, 100, 140, 180, 220]
list_out = []
for line in input:
if cycle in cycles:
list_out.append(cycle*X)
if line != "noop":
# match digit
match = re.search(" (.*\d*)$", line)
digit = int(match.group(0))
cycle += 1
if cycle in cycles:
list_out.append(cycle*X)
X += digit
cycle += 1
print("Solution 1:", sum(list_out))
Part 2… well, it’s a bit complicated to explain. Basically, it requires you print some characters depending on the value of X
before.
Here’s my solution:
X = 1
cycle = 1
pixel = 0
crt = []
list_X = []
def draw_crt(cycle, X, crt):
pixel_position = divmod((cycle-1), 40)
sprite_position = [X-1, X, X+1]
if pixel_position[1] in sprite_position:
crt.append("#")
else:
crt.append(".")
if pixel_position[1] == 39:
crt.append("\n")
return crt
for line in input:
crt = draw_crt(cycle, X, crt)
if line != "noop":
# match digit
match = re.search(" (.*\d*)$", line)
digit = int(match.group(0))
cycle += 1
crt = draw_crt(cycle, X, crt)
X += digit
cycle += 1
crt = "".join(crt)
print("Solution 2:")
print(crt)
And that’s it! 10 out of 25! This one was easier than the latter ones - I think I spent more time trying to get the prompt of part two than actually writing it.
Day 11
The input looks like this:
Monkey 0:
Starting items: 79, 98
Operation: new = old * 19
Test: divisible by 23
If true: throw to monkey 2
If false: throw to monkey 3
Monkey 1:
Starting items: 54, 65, 75, 74
Operation: new = old + 6
Test: divisible by 19
If true: throw to monkey 2
If false: throw to monkey 0
Monkey 2:
Starting items: 79, 60, 97
Operation: new = old * old
Test: divisible by 13
If true: throw to monkey 1
If false: throw to monkey 3
Monkey 3:
Starting items: 74
Operation: new = old + 3
Test: divisible by 17
If true: throw to monkey 0
If false: throw to monkey 1
The problem is 1) parsing the notes, 2) creating a monkey class with that info and 3) making the inspect/throw functions.
My solution for this one felt way cleaner than the one I took for the similarly class
problem Day 9:
import re
import math
with open("ex_input.txt") as file:
input = file.read()
input = input.split("\n\n")
class monkey():
def __init__(self, id, items, operation, test, passed, failed):
self.id = id
self.items = items
self.operation = operation
self.test = int(test)
self.passed = int(passed)
self.failed = int(failed)
self.inspected = 0
def inspect(self):
for item in self.items:
self.inspected += 1
worry = eval(self.operation.replace("old", str(item)))//3
if worry%self.test == 0:
monkey_list[self.passed].items.append(worry)
else:
monkey_list[self.failed].items.append(worry)
self.items = []
def monkey_business(self):
self.inspect()
def parse_notes(note):
"""
parses monkey notes
"""
monkey_id = re.search("Monkey (\d*)", note).group(1)
# parse items
items = re.search("items: ([\d ,]*)", note).group(1)
items = items.split(", ")
operation = re.search("new = (.*)", note).group(1)
test = re.search("by (\d*)", note).group(1)
true_cond = re.search("true: throw to monkey (\d*)", note).group(1)
false_cond = re.search("false: throw to monkey (\d*)", note).group(1)
return monkey_id, items, operation, test,true_cond, false_cond
# parse notes, asign to a list of monkeys
notes = []
for n_monkeys in range(len(input)):
notes.append(parse_notes(input[n_monkeys]))
monkey_list = [monkey(*notes[x]) for x in range(len(notes))]
# 20 rounds
for _ in range(20):
for monkey in monkey_list:
monkey.inspect()
# get inspection metric
inspected = []
for monkey in monkey_list:
inspected.append(monkey.inspected)
sorted_inspected = sorted(inspected)
print("Solution 1:", math.prod(sorted_inspected[-2:]))
The second part involves no longer dividing by 3 the worry
metric, and increasing the rounds to 10000
, resulting in slow computation times and big integer overflow.
I admit I had to check hints on this one because my maths game is very very weak. Anyway, in the end it turns out it’s a matter of computing the least common multiple to keep the validation of tests ok without feeding the operation monstruous integers. Makes sense. The implementation is quite trivial: modify the monkey function inspect()
to divide by the LCM
:
def inspect(self, lcm):
for item in self.items:
self.inspected += 1
worry = eval(self.operation.replace("old", str(item)))%lcm
if worry%self.test == 0:
monkey_list[self.passed].items.append(worry)
else:
monkey_list[self.failed].items.append(worry)
self.items = []
and instead of the 20 rounds, compute the LCM of the tests of all monkeys, feed it into inspect
, then increase rounds to 10000
:
lcm = []
for monkey in monkey_list:
lcm.append(monkey.test)
lcm = math.prod(lcm)
for _ in range(10000):
for monkey in monkey_list:
monkey.inspect(lcm)
The rest is the same.
I should have realized before, but well… First time I find a similar problem. A signal that I should go back to math principles.