In [15]:
def readfile(path):
    with open(path) as input_file: 
        return input_file.read().strip()
In [157]:
memo3 = {}

def find_nearest(grid, current):
    gridkey = "".join(grid.values())
    if (memoized := memo3.get((gridkey, current))):
        return memoized

    q = [(current, 0)]
    keys = {}
    doors = {}
    visited = set()
    while q:
        cur, d = q.pop(0)
        for i, j in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
            x, y = cur[0] + i, cur[1] + j
            if (x, y) in visited:
                continue
            visited.add((x, y))

            ch = grid.get((x, y))
            if ch == '.' or ch == '@' or (ch >= '0' and ch <= '3'):
                q.append(((x, y), d + 1))
            elif ch >= 'a' and ch <= 'z':
                keys[ch] = ((x, y), d + 1)
                q.append(((x, y), d + 1))
            elif ch >= 'A' and ch <= 'Z':
                doors[ch] = ((x, y), d + 1)

    result = keys, doors
    memo3[(gridkey, current)] = result
    return result

def gridmaker(inp):
    grid = {}
    points = {}

    x, y = 0, 0
    bot = 0
    for c in inp:
        if c != '#' and c != '.' and c != '\n':
            if c == '@':
                c = str(bot)
                bot += 1
            points[c] = (x, y)

        if c == '\n':
            y += 1
            x = 0
        else:
            grid[(x, y)] = c
            x += 1
    return grid, points

def printgrid(grid):
    minx = min(x for (x, _) in grid.keys())
    maxx = max(x for (x, _) in grid.keys())
    miny = min(y for (_, y) in grid.keys())
    maxy = max(y for (_, y) in grid.keys())

    for y in range(miny, maxy + 1):
        for x in range(minx, maxx + 1):
            print(grid.get((x, y)), end='')
        print()
    
# memo = {}

def keyfinder(grid, points, point, opened=""):
    # if (point, opened) in memo:
    #     return memo[(point, opened)]

    keys, doors = find_nearest(grid, point)
    min_distance = None
    min_opened = ""
    min_pos = point

    for label, (pos, dist) in keys.items():
        if min_distance is not None and dist > min_distance: 
            continue

        nextgrid = grid.copy()
        if (door := label.upper()) in points:
            nextgrid[points[door]] = '.' # Open door
        nextgrid[pos] = '.'

        distance, new_opened, last_pos = keyfinder(nextgrid, points, pos, "".join(sorted(opened + label))) 
        distance += dist

        if not min_distance or distance < min_distance:
            min_distance = distance
            min_opened = new_opened
            min_pos = last_pos

    # memo[(point, opened)] = (min_distance or 0, "".join(sorted(opened + min_opened)), min_pos)
    return (min_distance or 0, "".join(sorted(opened + min_opened)), min_pos)
    # return memo[(point, opened)]
In [154]:
memo2 = {}

def botfinder2(grid, points):
    gridkey = "".join(grid.values())
    pointskey = "-".join([f"{l}.{x}.{y}" for l, (x, y) in points.items()])
    if (memoized := memo2.get((gridkey, pointskey))):
        return memoized

    accessible_keys = []
    for point in range(4):
        keys, doors = find_nearest(grid, points[str(point)])
        accessible_keys.extend((str(point), x[0], x[1]) for x in keys.items())

    min_distance = None

    for pt, label, (pos, dist) in accessible_keys:
        if min_distance is not None and dist > min_distance: 
            continue

        nextgrid = grid.copy()
        nextpoints = points.copy()

        if (door := label.upper()) in points:
            nextgrid[points[door]] = '.' # Open door
        nextgrid[pos] = '.'
        nextpoints[pt] = pos

        distance = botfinder2(nextgrid, nextpoints) + dist

        if not min_distance or distance < min_distance:
            min_distance = distance

    result = min_distance or 0
    memo2[(gridkey, pointskey)] = result
    return result
    
In [158]:
memo2, memo3 = {}, {}
botfinder2(*gridmaker("""#######
#a.#Cd#
##@#@##
#######
##@#@##
#cB#.b#
#######"""))
Out[158]:
8
In [145]:
memo2 = {}
botfinder2(*gridmaker("""###############
#d.ABC.#.....a#
######@#@######
###############
######@#@######
#b.....#.....c#
###############"""))
Out[145]:
24
In [146]:
memo2 = {}
botfinder2(*gridmaker("""#############
#DcBa.#.GhKl#
#.###@#@#I###
#e#d#####j#k#
###C#@#@###J#
#fEbA.#.FgHi#
#############"""))
Out[146]:
32
In [159]:
memo2, memo3 = {}, {}
botfinder2(*gridmaker("""#############
#g#f.D#..h#l#
#F###e#E###.#
#dCba@#@BcIJ#
#############
#nK.L@#@G...#
#M###N#H###.#
#o#m..#i#jk.#
#############"""))
Out[159]:
72
In [160]:
memo2, memo3 = {}, {}
botfinder2(*gridmaker(readfile("AoC/input18_2.txt").strip()))
Out[160]:
1988