Advent of Code - 2015
This is a solution to Day 6 of Advent of Code 2015.
Day 6 - Probably a Fire Hazard
Because your neighbors keep defeating you in the holiday house decorating contest year after year, you've decided to deploy one million lights in a 1000x1000 grid.
Furthermore, because you've been especially nice this year, Santa has mailed you instructions on how to display the ideal lighting configuration.
Lights in your grid are numbered from 0 to 999 in each direction; the lights at each corner are at 0,0, 0,999, 999,999, and 999,0. The instructions include whether to turn on, turn off, or toggle various inclusive ranges given as coordinate pairs. Each coordinate pair represents opposite corners of a rectangle, inclusive; a coordinate pair like 0,0 through 2,2 therefore refers to 9 lights in a 3x3 square. The lights all start turned off.
To defeat your neighbors this year, all you have to do is set up your lights by doing the instructions Santa sent you in order.
For example:
turn on 0,0 through 999,999
would turn on (or leave on) every light.toggle 0,0 through 999,0
would toggle the first line of 1000 lights, turning off the ones that were on, and turning on the ones that were off.turn off 499,499 through 500,500
would turn off (or leave off) the middle four lights.After following the instructions, how many lights are lit?
Read input
For the first time, we need a bit more complex input reading.
I use namedtuple to store individual values in easy to use format.
Add a bit of regex to catch individual values and a bit of int conversions and we have a beautiful list of Instruction
tuples to work with.
I like to keep all this input parsing in its own block to avoid number parsing and string splitting inside the main logic.
from collections import namedtuple
import re
from utils import read_input
Instruction = namedtuple('Instruction', ['action', 'start', 'end'])
def transformer(line):
matches = re.match(r'(turn on|turn off|toggle) (\d+),(\d+) through (\d+),(\d+)', line).groups()
action = matches[0]
start = (int(matches[1]), int(matches[2]))
end = (int(matches[3]), int(matches[4]))
return Instruction(action=action, start=start, end=end)
instructions = read_input(6, transformer)
Since every light is either on or not, we can use set
to just keep track of lit lights. This way we don't actually need to keep track of the full 1000x1000 grid.
Another feature I'm using here is new to Python 3.10 and it's fantastic. It's called Pattern Matching and I really like how it works with namedtuples.
What
match instruction:
case Instruction(action='turn on', start=start, end=end)
actually does is two (well, three) things:
- It's matching our
instruction
to be namedtupleInstruction
- It's matching the
action
of ourinstruction
to be string literal "turn on" - It's mapping values of
start
andend
into variables namedstart
andend
Without pattern matching, we'd have to do something like this:
if type(instruction) == Instruction and instruction.action == 'turn on':
start = instruction.start
end = instruction.end
and I'll take the first over the second any day of the week. (We most likely wouldn't actually type the type(instruction) == Instruction
part in the if due to duck typing and so on but I wanted to add it there for completion's sake).
def turn_on(lit, start, end):
for row in range(start[0], end[0] + 1):
for col in range(start[1], end[1] + 1):
lit.add((col, row))
return lit
def turn_off(lit, start, end):
for row in range(start[0], end[0] + 1):
for col in range(start[1], end[1] + 1):
if (col, row) in lit:
lit.remove((col, row))
return lit
def toggle(lit, start, end):
for row in range(start[0], end[0] + 1):
for col in range(start[1], end[1] + 1):
if (col, row) in lit:
lit.remove((col, row))
else:
lit.add((col, row))
return lit
def switch_lights(instructions):
lit = set()
for instruction in instructions:
match instruction:
case Instruction(action="turn on", start=start, end=end):
lit = turn_on(lit, start, end)
case Instruction(action="turn off", start=start, end=end):
lit = turn_off(lit, start, end)
case Instruction(action="toggle", start=start, end=end):
lit = toggle(lit, start, end)
return lit
result = len(switch_lights(instructions))
print(f'Solution: {result}')
assert result == 543903
Part 2
You just finish implementing your winning light pattern when you realize you mistranslated Santa's message from Ancient Nordic Elvish.
The light grid you bought actually has individual brightness controls; each light can have a brightness of zero or more. The lights all start at zero.
The phrase
turn on
actually means that you should increase the brightness of those lights by 1.The phrase
turn off
actually means that you should decrease the brightness of those lights by 1, to a minimum of zero.The phrase
toggle
actually means that you should increase the brightness of those lights by 2.What is the total brightness of all lights combined after following Santa's instructions?
For example:
turn on 0,0 through 0,0
would increase the total brightness by 1.toggle 0,0 through 999,999
would increase the total brightness by 2000000.
For this part, I'm switching from set
to defaultdict
to store individual brightness.
This also enables me to have only one function for adjusting the brightness of individual lights.
from collections import defaultdict
def adjust(lit, start, end, volume):
for row in range(start[0], end[0] + 1):
for col in range(start[1], end[1] + 1):
lit[(col, row)] += volume
if lit[(col, row)] < 0: # Brightness can't go below 0
lit[(col, row)] = 0
return lit
def adjust_brightness(instructions):
lit = defaultdict(int)
for instruction in instructions:
match instruction:
case Instruction(action="turn on", start=start, end=end):
lit = adjust(lit, start, end, volume=1)
case Instruction(action="turn off", start=start, end=end):
lit = adjust(lit, start, end, volume=-1)
case Instruction(action="toggle", start=start, end=end):
lit = adjust(lit, start, end, volume=2)
return lit
result = sum(adjust_brightness(instructions).values())
print(f'Solution: {result}')
assert result == 14687245