Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2020-12-24 - Lobby Layout

(original .ipynb)

Day 24 input is a sequence of paths through a hexagonal grid (mine is here) representing the tile pattern of a floor (once the end of each path is reached the tile color is flipped). Part 1 involves interpreting these paths and determining how many end up being black. Part 2 involves playing this repeatedly with a few cellular-automata/game-of-life type rules that govern how the tiles change over time.

from collections import defaultdict
from pprint import pprint

test_input_str = """sesenwnenenewseeswwswswwnenewsewsw
neeenesenwnwwswnenewnwwsewnenwseswesw
seswneswswsenwwnwse
nwnwneseeswswnenewneswwnewseswneseene
swweswneswnenwsewnwneneseenw
eesenwseswswnenwswnwnwsewwnwsene
sewnenenenesenwsewnenwwwse
wenwwweseeeweswwwnwwe
wsweesenenewnwwnwsenewsenwwsesesenwne
neeswseenwwswnwswswnw
nenwswwsewswnenenewsenwsenwnesesenew
enewnwewneswsewnwswenweswnenwsenwsw
sweneswneswneneenwnewenewwneswswnese
swwesenesewenwneswnwwneseswwne
enesenwswwswneneswsenwnewswseenwsese
wnwnesenesenenwwnenwsewesewsesesew
nenewswnwewswnenesenwnesewesw
eneswnwswnwsenenwnwnwwseeswneewsenese
neswnwewnwnwseenwseesewsenwsweewe
wseweeenwnesenwwwswnew"""

puzzle_input_str = open("puzzle_input/day24.txt", "r").read()


def parse_input_line(input_line_str):
    input_line = [c for c in input_line_str]
    instructions = []
    previous = None
    while len(input_line) > 0:
        current = input_line.pop(0)
        if current == "n" or current == "s":
            previous = current
        elif current == "e":
            if previous:
                current = previous + current
                previous = None
        elif current == "w":
            if previous:
                current = previous + current
                previous = None
        if not previous:
            instructions.append(current)
    return instructions


def next_row(row, direction):
    if direction == "ne" or direction == "nw":
        return row - 1
    elif direction == "se" or direction == "sw":
        return row + 1
    elif direction == "e" or direction == "w":
        return row

def next_col(row, col, direction):
    even_row = row % 2 == 0
    
    if direction == "ne":
        if even_row:
            return col 
        else:
            return col + 1
        
    elif direction == "nw":
        if even_row:
            return col - 1
        else:
            return col
        
    elif direction == "se":
        if even_row:
            return col
        else:
            return col + 1
        
    elif direction == "sw":
        if even_row:
            return col - 1
        else:
            return col
        
    elif direction == "e":
        return col + 1
        
    elif direction == "w":
        return col - 1
        
    else:
        raise Exception(f"Invalid instruction {instruction}")
        
def map_floor_pattern(input_str):
    all_flip_sequences = [parse_input_line(input_line) for input_line in input_str.split("\n")]
    floor_pattern_map = defaultdict(lambda:True)    
    
    for flip_sequence in all_flip_sequences:
        row = 0
        col = 0
        for instruction in flip_sequence:
            row = next_row(row, instruction)
            col = next_col(row, col, instruction)
        coord = (row, col)
        floor_pattern_map[coord] = not floor_pattern_map[coord]

    return floor_pattern_map

def count_black_tiles(floor_pattern_map):
    number_black = 0
    for coord in floor_pattern_map:
        if not floor_pattern_map[coord]:
            number_black += 1
    return number_black

def part1(input_str):
    floor_pattern_map = map_floor_pattern(input_str)
    return count_black_tiles(floor_pattern_map)
    
assert  1 == part1("nwwswee")
assert 10 == part1(test_input_str)

print(part1(puzzle_input_str))
459
%%time
# ^^ adding this to show my solution is s-l-o-w

import math

directions = ["ne", "nw", "se", "sw", "e", "w"]

def get_neighbours(grid, row, col):
    neighbour_coords = [
        (next_row(row, direction), next_col(row, col, direction))
        for direction
        in directions        
    ]
    
    max_row = len(grid)
    max_col = len(grid[0])
    
    neighbours = []
    for direction in directions:
        r = next_row(row, direction)
        c = next_col(row, col, direction)
        if r < 0 or r >= max_row or c < 0 or c >= max_col:
            continue
        
        neighbours.append(grid[r][c])
        
    return neighbours
            

def generate_grid(rows, cols):
    grid = []
    for r in range(rows + 1):
        grid.append([True for c in range(cols + 1)])
    return grid


def grid_from_map(floor_pattern_map):
    min_row, min_col = math.inf, math.inf
    max_row, max_col = -math.inf, -math.inf
    
    for row, col in floor_pattern_map:
        if row < min_row:
            min_row = row
        elif row > max_row:
            max_row = row
            
        if col < min_col:
            min_col = col
        elif col > max_col:
            max_col = col
            
    # our grid will be (max_row - min_row) rows and (max_col - min_col) columns
    grid = generate_grid(
        max_row - min_row,
        max_col - min_col
    )
    
    # now set all appropriate vals
    for coord in floor_pattern_map:
        row, col = coord
        adjusted_row = row - min_row
        adjusted_col = col - min_col
        grid[adjusted_row][adjusted_col] = floor_pattern_map[coord]
    
    return grid

def create_empty_row(grid):
    return [True] + [True for tile in grid[0]] + [True]

def extend_row(row, grow_left, grow_right):
    new_row = []
    
    if grow_left:
        new_row.append(True)
        
    new_row += row
        
    if grow_right:
        new_row.append(True)
    
    return new_row
    
def maybe_grow_grid(grid):
    grow_up = any(grid[0])
    grow_down = any(grid[-1])
    grow_left = any([row[0] for row in grid])
    grow_right = any([row[-1] for row in grid])
    
    if not any([grow_up, grow_down, grow_left, grow_right]):
        return grid
    
    new_grid = []

    if grow_up:
        new_grid.append(create_empty_row(grid))
        new_grid.append(create_empty_row(grid))
        
    new_grid += [extend_row(row, grow_left, grow_right) for row in grid]
    
    if grow_down:
        new_grid.append(create_empty_row(grid))
        new_grid.append(create_empty_row(grid))
    
    return new_grid
    
def step(grid):
    new_grid = []
    for r, row in enumerate(grid):
        new_row = []
        for c, tile in enumerate(row):
            neighbours = get_neighbours(grid, r, c)
            black_neighbours = sum(1 for n in neighbours if not n)
                
            # Any white tile with exactly 2 black tiles immediately adjacent to it is flipped to black.
            if tile and black_neighbours == 2:
                tile = False
                
            # Any black tile with zero or more than 2 black tiles immediately adjacent to it is flipped to white.
            elif (not tile) and (black_neighbours > 2 or black_neighbours == 0):
                tile = True
            
            new_row.append(tile)
    
        new_grid.append(new_row)                        
        
    return new_grid

def count_black_grid_tiles(grid):
    count = 0
    for row in grid:
        for tile in row:
            if not tile:
                count += 1
    return count           

def part2(input_str, generations):
    floor_pattern_map = map_floor_pattern(input_str)
    floor_grid = grid_from_map(floor_pattern_map)

    while generations > 0:
        floor_grid = step(maybe_grow_grid(floor_grid))
        generations -= 1
        
    return count_black_grid_tiles(floor_grid)
    
assert 2208 == part2(test_input_str, 100)
part2(puzzle_input_str, 100)
CPU times: user 1min 7s, sys: 139 ms, total: 1min 7s
Wall time: 1min 9s
4150