Sean McLemon | Advent of Code

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


2022-12-12 - Hill Climbing Algorithm

(original .ipynb)

Day 12 puzzle input is a grid of letters with the lower-case letters representing heights (a being the lowers, z being the highest) as well as the start point S and the destination E which have heights "a" and "z" respectively (mine is here). Part 1 involves finding the shortest path from S to Z (where you can only travel "up" one height unit). Part 2 involves finding the shortest path from any a-height point to E.

%%time

from advent import neighbours4
from math import inf

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

test_input_str = """
Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
""".strip()


def get_height_val(col):
    if col == "S":
        return ord("a")
    elif col == "E":
        return ord("z")
    return ord(col)    


def parse_input(input_str):
    start, end = None, None
    grid = []
    for r, row in enumerate(input_str.splitlines()):
        if "S" in row:
            start = (r, row.index("S"))
        if "E" in row:
            end = (r, row.index("E"))
        grid.append([get_height_val(col) for col in row])
    return start, end, grid


def can_reach(grid, r, c, nr, nc):
    return grid[nr][nc] <= grid[r][c] + 1


def build_shortest_paths_grid(grid, from_node, reachable=can_reach):
    shortest_paths = [
        [inf for _ in row]
        for row in grid
    ]    
    queue = [(from_node, 0)]
    
    while len(queue) > 0:
        (r, c), steps = queue.pop(0)
        
        if steps >= shortest_paths[r][c]:
            continue
        
        shortest_paths[r][c] = steps
        queue += [
            [(nr,nc), steps+1]
            for nr,nc in neighbours4(grid, r, c)
            if reachable(grid, r, c, nr, nc)
        ]

    return shortest_paths


def part_one(input_str):
    start, end, grid = parse_input(input_str)
    shortest_paths = build_shortest_paths_grid(grid, start)
    end_r, end_c = end
    return shortest_paths[end_r][end_c]


assert 31 == part_one(test_input_str)

print("part one:", part_one(puzzle_input_str))
part one: 350
CPU times: user 24.1 ms, sys: 3.98 ms, total: 28 ms
Wall time: 26.9 ms
%%time

def can_reach_from(grid, r, c, nr, nc):
    return can_reach(grid, nr, nc, r, c)


def part_two(input_str):
    start, end, grid = parse_input(input_str)
    shortest_paths = build_shortest_paths_grid(grid, end, can_reach_from)
    a_path_lengths = []
    for r, row in enumerate(grid):
        for c, col in enumerate(row):
            if col == ord("a"):
                a_path_lengths.append(shortest_paths[r][c])
    return min(a_path_lengths)


assert 29 == part_two(test_input_str)

print("part two:", part_two(puzzle_input_str))
part two: 349
CPU times: user 25.7 ms, sys: 3.72 ms, total: 29.4 ms
Wall time: 28.7 ms