2022 Advent of Code
I'm having a go at the 2022 Advent of Code. Here's how I solved the problems - the ones I could be bothered solving anyway.
01
Use this awk script to convert the data to a CSV, one row per elf:
/[0-9]/ { printf "%d,", $0 }
/^$/ { print "" }
Load the result into a spreadsheet application, sum the rows, and sort the totals to get the answers.
02
The score depends only on the second character (X, Y or Z), and whether you win, lose or draw. I went for Python this time.
import sys
import functools
player_score = { 'X': 1, 'Y': 2, 'Z': 3 }
win_scores = {
'X': { 'A': 3, 'B': 0, 'C': 6 },
'Y': { 'A': 6, 'B': 3, 'C': 0 },
'Z': { 'A': 0, 'B': 6, 'C': 3 },
}
print (functools.reduce(
lambda score, line: score + player_score[line[2]] + win_scores[line[2]][line[0]],
(l for l in sys.stdin if len(l) >= 3),
0
))
Part two is a little simpler, you can precalculate each result:
import sys
import functools
scores = {
'A': { 'X': 0 + 3, 'Y': 3 + 1, 'Z': 6 + 2 },
'B': { 'X': 0 + 1, 'Y': 3 + 2, 'Z': 6 + 3 },
'C': { 'X': 0 + 2, 'Y': 3 + 3, 'Z': 6 + 1 },
}
print (functools.reduce(
lambda score, line: score + scores[line[0]][line[2]],
(l for l in sys.stdin if len(l) >= 3),
0
))
03
Get the first half of the string, look for the first character in the second half in the first half
import sys
import functools
# the score of each item; a = 1
scores = list(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
def score(line):
half = len(line) // 2
first_half = line[0:half]
try:
common = next(l for l in line[half:] if l in first_half)
return scores.index(common)
except StopIteration:
# no item in common
return 0
print(functools.reduce(
lambda s, line: s + score(line.strip()),
sys.stdin,
0
))
Part 2:
import sys
import functools
import itertools
# the score of each item; a = 1
scores = list(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
def score(lines):
rest = lines[1:]
common = next(c for c in lines[0] if all(c in l for l in rest))
return scores.index(common)
print(functools.reduce(
lambda s, lines: s + score([l.strip() for l in lines]),
# take groups of 3 until the result is empty
iter(lambda: list(itertools.islice(sys.stdin, 3)), []),
0
))
04
import sys
import re
line_re = re.compile(r'(\d+)-(\d+),(\d+)-(\d+)')
def within(line):
n = [int(n) for n in line_re.match(line).group(1, 2, 3, 4)]
return n[0] >= n[2] and n[1] <= n[3] or \
n[2] >= n[0] and n[3] <= n[1]
print(sum(
within(line) and 1 or 0 for line in sys.stdin
))
Part 2 is the same, with the return value:
return n[0] <= n[3] and n[1] >= n[2] or \
n[1] <= n[2] and n[0] >= n[3]
05
The stacks are represented by lists. The initial state is loaded into the start of each list, then manipulated at the end of the lists.
import sys
import re
move_re = re.compile('move (\d*) from (\d*) to (\d*)')
stack_count = 9
stacks = [[] for n in range(stack_count)]
for line in sys.stdin:
if len(line) < 36:
break
for n in range(stack_count):
char = line[n * 4 + 1]
if char != ' ':
stacks[n].insert(0, char)
for line in sys.stdin:
match = move_re.match(line)
if not match:
continue
number, src, to = (int(n) for n in move_re.match(line).group(1, 2, 3))
transferred = stacks[src-1][-number:]
transferred.reverse()
del stacks[src-1][-number:]
stacks[to-1].extend(transferred)
print("".join(s[-1] for s in stacks))
For part two, remove the reverse
line (which I did first, because I didn't read the instructions properly!)
06
Almost a one liner:
import sys
data = sys.stdin.read()
count = 4
print(next(n for n in range(count, len(data)) if len(set(data[n-count:n])) == count))
Change count to 14 for the second step.
07
import sys
root = {}
dir = root
for line in sys.stdin:
if line.startswith("$ cd "):
name = line[5:-1] # chop off the newline
if name == '/':
dir = root
else:
dir = dir[name]
elif not line.startswith("$"):
size, name = line.split(maxsplit=1)
name = name[:-1] # chop off the newline
if size == "dir":
dir.setdefault(name, {"..": dir})
else:
dir[name] = int(size)
def sizes(dir, all=[]):
total = 0
for name, val in dir.items():
if name == '..':
continue
if type(val) == dict:
total += sizes(val, all)[-1]
else:
total += val
all.append(total)
return all
print(sum(s for s in sizes(root) if s <= 100000))
For part 2, change the final print
to
allsizes = sizes(root)
used = allsizes[-1]
required = 30000000 - (70000000 - used)
print(min(s for s in sizes(root) if s >= required))
08
Create iterators extending from each side of the grid. Store the coordinates of seen trees in a set.
import sys
grid = []
for line in sys.stdin:
grid.append([int(n) for n in line if n.isdigit()])
visible = set()
def look(iter):
highest = -1
for coord, value in iter:
if value > highest:
visible.add(coord)
highest = value
for y, line in enumerate(grid):
look(((x, y), line[x]) for x in range(len(line) - 1))
look(((x, y), line[x]) for x in range(len(line) - 1, 0, -1))
for x in range(len(grid[0]) - 1):
look(((x, y), grid[y][x]) for y in range(len(grid) - 1))
look(((x, y), grid[y][x]) for y in range(len(grid) - 1, 0, -1))
print(len(visible))
09
import sys
visited = set((0,0))
head = (0, 0)
tail = (0, 0)
dirs = {
'U': lambda x: (x[0], x[1] - 1),
'D': lambda x: (x[0], x[1] + 1),
'L': lambda x: (x[0] - 1, x[1]),
'R': lambda x: (x[0] + 1, x[1]),
}
signum = lambda x: x < 0 and -1 or x > 0 and 1 or 0
for line in sys.stdin:
direction, count = line.split()
dir_fn = dirs[direction]
for n in range(int(count)):
head = dir_fn(head)
dx = head[0] - tail[0]
dy = head[1] - tail[1]
if abs(dx) > 1 or abs(dy) > 1:
tail = (tail[0]+signum(dx), tail[1]+signum(dy))
visited.add(tail)
print(len(visited))
Apparently this is wrong. I took a guess and subtracted one, and that was right. It looks like I got the constructor call for visited
wrong.
The second part involves extending it to a rope of 10 elements:
import sys
visited = set()
visited.add((0,0))
rope = [(0, 0)] * 10
dirs = {
'U': lambda x: (x[0], x[1] - 1),
'D': lambda x: (x[0], x[1] + 1),
'L': lambda x: (x[0] - 1, x[1]),
'R': lambda x: (x[0] + 1, x[1]),
}
signum = lambda x: x < 0 and -1 or x > 0 and 1 or 0
for line in sys.stdin:
direction, count = line.split()
dir_fn = dirs[direction]
for n in range(int(count)):
rope[0] = dir_fn(rope[0])
for n in range(0,9):
a = rope[n]
b = rope[n+1]
dx = a[0] - b[0]
dy = a[1] - b[1]
if abs(dx) > 1 or abs(dy) > 1:
rope[n+1] = (b[0]+signum(dx), b[1]+signum(dy))
visited.add(rope[-1])
print(len(visited))
10
import sys
import unittest
def run(lines):
x = 1
for line in lines:
if line.startswith("noop"):
yield x
elif line.startswith("addx "):
x = x + int(line[5:])
yield x
yield x
class TestExecution(unittest.TestCase):
def test_example(self):
self.assertEqual([1, 4, 4, -1, -1], list(run(['noop', 'addx 3', 'addx -5'])))
unittest.main(exit=False)
# Subtract two from the index for the delay, one more because
# it's 1 indexed
print(sum(x * (c+3) for c, x in enumerate(run(sys.stdin)) if c in range(17, 221, 40)))
For the second part, change the last line to:
out = itertools.chain([1, 1], run(sys.stdin))
for y in range(6):
for x in range(40):
val = next(out)
print((x >= val-1 and x <= val+1) and '#' or '.', end='')
print()
11
This one was tough - there are plenty of chances to misread the problem and type the wrong data.
import sys
class Monkey:
def __init__(self, num, items, op, divisor, testtrue, testfalse):
self.num = num
self.items = items
self.op = op
self.divisor = divisor
self.testtrue = testtrue
self.testfalse = testfalse
self.inspections = 0
def inspect(self, monkeys):
items = self.items
self.items = []
self.inspections = self.inspections + len(items)
for i in items:
w = self.op(i) // 3
if (w % self.divisor) == 0:
monkeys[self.testtrue].items.append(w)
else:
monkeys[self.testfalse].items.append(w)
monkeys = [
Monkey(0, [83, 97, 95, 67], lambda x: x * 19, 17, 2, 7),
Monkey(1, [71, 70, 79, 88, 56, 70], lambda x: x + 2, 19, 7, 0),
Monkey(2, [98, 51, 51, 63, 80, 85, 84, 95], lambda x: x + 7, 7, 4, 3),
Monkey(3, [77, 90, 82, 80, 79], lambda x: x + 1, 11, 6, 4),
Monkey(4, [68], lambda x: x * 5, 13, 6, 5),
Monkey(5, [60, 94], lambda x: x + 5, 3, 1, 0),
Monkey(6, [81, 51, 85], lambda x: x * x, 5, 5, 1),
Monkey(7, [98, 81, 63, 65, 84, 71, 84], lambda x: x + 3, 2, 2, 3),
]
for x in range(20):
for monkey in monkeys:
monkey.inspect(monkeys)
inspections = [m.inspections for m in monkeys]
inspections.sort()
print(inspections[-1] * inspections[-2])
Using this approach for part two doesnt work - the numbers get too big.
12
Looks interesting, it's essentially a maze solving algorithm.
The algorithm would be something like:
- Let "Set" be a class containing a coordinate and a direction.
- Let there be a list of Setps.
- The directions are, in sequence, up, right, down and left.
- You can't go further if you hit an edge, hit a space too high, or a space in the list of steps.
- While you can continue to move up:
- Add the next step up to the list of steps
- Go the next direction, then continue the above step
- If you can't move, remove one Step from the list of Steps, then continue testing the directions.
- Continue until the end is reached.
After working on this for a while, it was taking a really long time to run, so I added the places visited but rejected to another list, and didn't visit them again. There was another problem: it can't account for a shorter way between two visited points. Instead, work from the end to the start, and for each point, store the minimum steps required to reach it from the end. Where a new minimum is set, re-evaluate all paths from that point, but there's no point visiting any spaces with a lower score. Thinking about these rules, instead of keeping a list of steps visited, when setting a new minimum, add that space to a list of spaces to be evaluated, and continue until this list is empty.
import sys
movements = [
lambda pos: (pos[0], pos[1] - 1), # up
lambda pos: (pos[0] + 1, pos[1]), # right
lambda pos: (pos[0], pos[1] + 1), # down
lambda pos: (pos[0] - 1, pos[1]), # left
]
class Terrain:
def __init__(self, data):
self.data = data
self.width = len(self.data[0])
self.height = len(self.data)
self.size = self.width * self.height
def __getitem__(self, pos):
return self.data[pos[1]][pos[0]]
def __setitem__(self, pos, val):
self.data[pos[1]][pos[0]] = val
terrain = Terrain([
[(1000 if c == 'S' else 26 if c == 'E' else ord(c) - ord('a')) for c in line[:-1]]
for line in sys.stdin
])
scores = Terrain([
[None] * terrain.width for i in range(terrain.height)
])
# I can't be bothered extracting these from the input
start = (0, 20)
end = (137, 20)
scores[end] = 0
to_evaluate = [end]
while len(to_evaluate):
curr = to_evaluate.pop()
score = scores[curr]
for movement in movements:
next = movement(curr)
if next[0] < 0 or next[0] >= terrain.width \
or next[1] < 0 or next[1] >= terrain.height:
continue
nextScore = scores[next]
if terrain[curr] > terrain[next] + 1:
# The step is too high
continue
if nextScore == None or nextScore > score + 1:
scores[next] = score + 1
to_evaluate.append(next)
print(scores[start])
For part two, as this loop runs, keep track of the lowest score where the elevation is the lowest.
16
This feels similar to day 12. Let class Valve have a flow rate, and a list of tunnels. Walk the graph as in the map in day 12 until the 30 minutes have passed, then backtrack and continue.
17
This one took way longer than it should have, because the unit test for hits
was inadequate!
import sys
import itertools
import unittest
class Rock:
def __init__(self, data):
self.data = data
self.width = max(len(d) for d in data)
self.height = len(data)
rocks = itertools.cycle([
# data is upside down
Rock([[True, True, True, True]]),
Rock([[False, True, False], [True, True, True], [False, True, False]]),
Rock([[True, True, True], [False, False, True], [False, False, True]]),
Rock([[True], [True], [True], [True]]),
Rock([[True, True], [True, True]])
])
class Chamber:
def __init__(self, rows = [], width = 7):
# The chamber, where the bottom is index 0, the next is 1 etc
self.rows = rows
self.width = width
# ypos is the height where rock[0] appears
def hits(self, rock, xpos, ypos):
if xpos < 0 or xpos > self.width - rock.width:
return True
for y in range(min(len(self.rows) - ypos, rock.height)):
if next((True for a, b in zip(self.rows[y + ypos][xpos:], rock.data[y]) if a & b), False):
return True
return False
def place(self, rock, xpos, ypos):
self.rows += [[False] * self.width for n in range(self.height, ypos + rock.height)]
for y, data in enumerate(rock.data):
for x, val in enumerate(data):
row = self.rows[y + ypos]
row[xpos + x] = row[xpos + x] or val
@property
def height(self):
return len(self.rows)
def dump(self):
for n in range(len(self.rows) - 1, -1, -1):
print(''.join('#' if x else '.' for x in self.rows[n]))
print()
class ChamberTestCase(unittest.TestCase):
def test_hits(self):
chamber = Chamber([[False, True, True, False]])
rock = Rock([[True, False], [True, False]])
self.assertTrue(chamber.hits(rock, 2, 0))
self.assertFalse(chamber.hits(rock, 0, 0))
# the rock is entirely outside the chamber
self.assertFalse(chamber.hits(rock, 1, 1))
class ChamberPlaceTestCase(unittest.TestCase):
def setUp(self):
self.chamber = Chamber([
[True, False, True, False],
[True, False, False, False]
], width = 4)
def test_place_over_existing(self):
self.chamber.place(Rock([[True, False], [False, True]]), 2, 0)
self.assertEqual(self.chamber.rows, [
[True, False, True, False], [True, False, False, True]
])
def test_place_over_new_row(self):
self.chamber.place(Rock([[True, False], [False, True]]), 0, 1)
self.assertEqual(self.chamber.rows, [
[True, False, True, False],
[True, False, False, False],
[False, True, False, False]
])
unittest.main(exit=False)
jets = itertools.cycle([
1 if c == '>' else -1 for c in sys.stdin.read() if c == '<' or c == '>'
])
chamber = Chamber()
for n in range(2022):
rock = next(rocks)
x = 2
y = chamber.height + 3
while True:
newx = x + next(jets)
x = x if chamber.hits(rock, newx, y) else newx
if y == 0 or chamber.hits(rock, x, y - 1):
break
y = y - 1
chamber.place(rock, x, y)
print(len(chamber.rows))