Sean McLemon | Advent of Code

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


2021-12-23 - Amphipod

(original .ipynb)

Day 23 puzzle input is the position of some amphipods inside some rooms and a little hallway connecting them all. Mine is here. Part 1 involves using some rules about where and when they can be moved to determine how many moves are required to return them to their proper room. Part 2 involves modifying the input by adding a couple more amphipods per-room and doing the same calculation.

Verdict: got the result in the end but P2's test eludes me, it's like 4k lower than expected :-/ Not super happy with the end result:

Will come back to this one again.

from advent import listify

puzzle_input_str = open("puzzle_input/day23.txt").read()

test_input_str = """#############
#...........#
###B#C#B#D###
  #A#D#C#A#
  #########"""

@listify
def parse_input(input_str):
    for line in input_str.split("\n"):
        yield list(line)

amphipod_energy = {
    "A": 1,
    "B": 10,
    "C": 100,
    "D": 1000
}

test_rooms = {
    "A": ["A","B"],
    "B": ["D","C"],
    "C": ["C","B"],
    "D": ["A","D"]
}

puzzle_rooms = {
    "A": ["D","B"],
    "B": ["D","C"],
    "C": ["A","C"],
    "D": ["A","B"]
}

hallway_adjacent = {
    "A":2,
    "B":4,
    "C":6,
    "D":8
}
cannot_move_to = set(hallway_adjacent.values())


def can_move_to_hallway(hallway, from_pos, to_pos, transitional=False):
    if from_pos == to_pos:
        return True
    
    if (not transitional) and (to_pos in cannot_move_to):
        return False
    
    if to_pos < 0 or to_pos > len(hallway)-1:
        return False
    
    pos_step = to_pos - from_pos        
    pos_step //= abs(pos_step)    
    
    for pos in range(from_pos+pos_step, to_pos+pos_step, pos_step):
        if hallway[pos] != ".":
            return False
    return True


def can_move_to_room(rooms, room, amphipod, room_size):
    room_contents = [a for a in rooms[room] if a != "."]
    
    if room != amphipod:
        return False

    if len(room_contents) == room_size:
        return False

    if len(room_contents) > 0 and room_contents[-1] != amphipod:
        return False
    
    steps = room_size - len(room_contents)
    return steps

def moves_from_hallway(amphipod, rooms, hallway, pos, room_size, transitional=False):
    hallway_moves = []
    room_moves = []
    max_range = 9 + room_size
    for move in range(-max_range, max_range, 1):
        if move == 0:
            continue
        if transitional and can_move_to_hallway(hallway, pos, pos + move):
            hallway_moves.append((pos + move, abs(move)))
            
    for room, hallway_pos in hallway_adjacent.items():
        if transitional:
            pass
        elif room != amphipod:
            continue
        else:
            if can_move_to_hallway(hallway, pos, hallway_pos, True):
                adjustment = abs(hallway_pos - pos)
                if room_steps := can_move_to_room(rooms, room, amphipod, room_size):
                    room_moves.append((room, adjustment + room_steps))
        
    
    return hallway_moves, room_moves


def moves_from_room(room, rooms, hallway, room_size):
    if len(rooms[room]) == 0:
        return [], []

    pos = hallway_adjacent[room]
    hallway_moves, room_moves = moves_from_hallway(rooms[room][-1], rooms, hallway, pos, room_size, True)
    
    steps_out = room_size + 1 - len(rooms[room])
    return [(p,s+steps_out) for p,s in hallway_moves], [(r,s+steps_out) for r,s in room_moves]


from heapq import heapify, heappush, heappop
from copy import deepcopy

def step(rooms, hallway, total_energy, room_size):
    for room in rooms:
        if is_room_completed(room, rooms[room], room_size):
            continue 
            
        if len(rooms[room]) == 0:
            continue
        hallway_moves, room_moves = moves_from_room(room, rooms, hallway, room_size)
        dbg_print(hallway_moves, room_moves)
        for pos, steps in hallway_moves:
            new_rooms = deepcopy(rooms)
            amphipod = new_rooms[room].pop()
            required_energy = steps * amphipod_energy[amphipod]            
            new_hallway = deepcopy(hallway)
            new_hallway[pos] = amphipod
            yield (total_energy + required_energy, new_hallway, new_rooms)
        for new_room, new_steps in room_moves:
            
            new_rooms = deepcopy(rooms)
            new_hallway = deepcopy(hallway)
            
            amphipod = new_rooms[room].pop()
            required_energy = new_steps * amphipod_energy[amphipod]
            new_rooms[new_room].append(amphipod)
                      
            yield (total_energy + required_energy, new_hallway, new_rooms)
            
    for pos in range(len(hallway)):
        amphipod = hallway[pos]
        if amphipod == ".":
            continue
            
        hallway_moves, room_moves = moves_from_hallway(amphipod, rooms, hallway, pos, room_size)
        dbg_print(hallway_moves, room_moves)
        for new_pos, new_steps in hallway_moves:
            required_energy = new_steps * amphipod_energy[amphipod]
            new_rooms = deepcopy(rooms)
            new_hallway = deepcopy(hallway)
            new_hallway[new_pos] = amphipod
            new_hallway[pos] = "."
            yield (total_energy + required_energy, new_hallway, new_rooms)
        
        for new_room, new_steps in room_moves:
            required_energy = new_steps * amphipod_energy[amphipod]
            new_rooms = deepcopy(rooms)
            new_hallway = deepcopy(hallway)
            new_rooms[new_room].append(amphipod)
            new_hallway[pos] = "."
            
            yield (total_energy + required_energy, new_hallway, new_rooms)
            

def serialize_state(hallway, rooms, room_size):
    rooms_str = []
    for _, room in rooms.items():
        r = "".join(room)
        rooms_str.append(r)
        rooms_str.append((room_size - len(r)) * ".")
    
    hallway_str = "".join(hallway)
    rooms_str = "".join(rooms_str)   
        
    return f"{hallway_str}|{rooms_str}"
            

def deserialize_state(state, room_size):
    hallway_str, rooms_str = state.split("|")
    hallway = list(hallway_str)
    rooms = {}
    for i, r in enumerate("ABCD"):
        room = []
        for n in range(room_size):
            amphipod = rooms_str[(room_size*i)+n]
            if amphipod != ".":
                room.append(amphipod)
            else:
                break
        rooms[r] = room
    return hallway,rooms


def is_room_completed(r, room, room_size):
    good = 0
    for a in room:
        good += 1 if a == r else 0
    return good == room_size

def completed_rooms(rooms):
    good = 0
    for r, room in rooms.items():
        for a in room:
            good += 1 if a == r else 0
    return good
%%time

from pprint import pprint
from collections import defaultdict
from math import inf

def part_one(initial_rooms, quiet=False):
    room_size = len(initial_rooms["A"])
    previous_states = defaultdict(lambda:inf)
    
    h = ["."] * 11
    q = [(0, serialize_state(h, initial_rooms, room_size))]
    heapify(q)

    states = 0
    while state := heappop(q):
        states  += 1
        (energy, state) = state
        hallway, rooms = deserialize_state(state, room_size)
        
        if completed_rooms(rooms) == (4 * room_size):
            return energy
        
        if completed_rooms(rooms) == (4 * room_size) - 1:
            if not quiet:
                print(state, energy)

        for new_energy, new_hallway, new_rooms in step(rooms, hallway, energy, room_size):
            new_state = serialize_state(new_hallway, new_rooms, room_size)
            if new_energy < previous_states[new_state]:
                previous_states[new_state] = new_energy
                heappush(q, (new_energy, new_state))
    
    return energy


# assert 12521 == part_one(test_rooms, quiet=True)
print("part one:", part_one(puzzle_rooms))
...D.......|AABBCCD. 12053
.D.........|AABBCCD. 12057
.D.........|AABBCCD. 12367
D..........|AABBCCD. 13057
D..........|AABBCCD. 13367
...D.......|AABBCCD. 14033
.....D.....|AABBCCD. 14053
.....D.....|AABBCCD. 14129
.D.........|AABBCCD. 14167
D..........|AABBCCD. 15167
.....D.....|AABBCCD. 16033
.......D...|AABBCCD. 16053
...C.......|AABBC.DD 17969
.........B.|AAB.CCDD 17993
..........B|AAB.CCDD 18001
..........B|AAB.CCDD 18003
.........D.|AABBCCD. 18033
.......D...|AABBCCD. 18033
.B.........|AAB.CCDD 18037
B..........|AAB.CCDD 18039
.........A.|A.BBCCDD 18043
..........A|A.BBCCDD 18044
.A.........|A.BBCCDD 18049
.B.........|AAB.CCDD 18049
.C.........|AABBC.DD 18049
A..........|A.BBCCDD 18050
...........|AABBCCDD 18051
part one: 18051
CPU times: user 17.9 s, sys: 27.8 ms, total: 17.9 s
Wall time: 18.2 s
%%time

test_rooms_2 = {
    "A": ["A", "D", "D", "B"],
    "B": ["D", "B", "C", "C"],
    "C": ["C", "A", "B", "B"],
    "D": ["A", "C", "A", "D"]
}

puzzle_rooms_2 = {
    "A": ["D", "D", "D", "B"],
    "B": ["D", "B", "C", "C"],
    "C": ["A", "A", "B", "C"],
    "D": ["A", "C", "A", "B"]  # you're god damn right
}

def part_two(initial_rooms):
    ans = part_one(initial_rooms)
    print(ans)
    return ans

# assert 44169 == part_two(test_rooms_2)
print("part two:", part_two(puzzle_rooms_2))
D..........|AAAABBBBCCCCDDD. 50061
..........A|AAA.BBBBCCCCDDDD 50236
A..........|AAA.BBBBCCCCDDDD 50242
...........|AAAABBBBCCCCDDDD 50245
50245
part two: 50245
CPU times: user 23 s, sys: 12 ms, total: 23 s
Wall time: 23.2 s