Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

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:

  1. It's matching our instruction to be namedtuple Instruction
  2. It's matching the action of our instruction to be string literal "turn on"
  3. It's mapping values of start and end into variables named start and end

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