Sean McLemon | Advent of Code

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


2022-12-16 - Proboscidea Volcanium

(original .ipynb)
from collections import defaultdict

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

test_input_str = """Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
Valve BB has flow rate=13; tunnels lead to valves CC, AA
Valve CC has flow rate=2; tunnels lead to valves DD, BB
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
Valve EE has flow rate=3; tunnels lead to valves FF, DD
Valve FF has flow rate=0; tunnels lead to valves EE, GG
Valve GG has flow rate=0; tunnels lead to valves FF, HH
Valve HH has flow rate=22; tunnel leads to valve GG
Valve II has flow rate=0; tunnels lead to valves AA, JJ
Valve JJ has flow rate=21; tunnel leads to valve II"""

def parse_rate(rate_str):
    _, rate = rate_str.split("=")
    return int(rate.rstrip(";"))


def parse_line(line):
    tokens = line.split(" ")
    return tokens[1], parse_rate(tokens[4]), [valve.rstrip(",") for valve in tokens[9:]]
    

def bfs_find(neighbours, v1, v2):
    queue = [(v1,0)]
    visited = {v1}
    while queue:
        (v, cost) = queue.pop(0)
        visited.add(v)
        if v == v2:
            return 1 + cost
        
        queue += [(n, cost+1) for n in neighbours[v] if n not in visited]
    raise Exception(f"couldn't find {v2}")
    
    
def build_distances_lookup(neighbours):
    distances = defaultdict(dict)
    for v1 in neighbours:
        for v2 in neighbours:
            distances[v1][v2] = bfs_find(neighbours, v1, v2)
            
    return distances
    
    
def part_one(input_str):
    valves = [parse_line(line) for line in input_str.splitlines()]
    flow_rates = {v[0]:v[1] for v in valves}
    neighbours = {v[0]:v[2] for v in valves}
    all_valves = {v[0] for v in valves if flow_rates[v[0]] != 0}
    time_to = build_distances_lookup(neighbours)
    
    max_pressure = 0
    queue = [("AA", 30, set(), 0)]
    
    while queue:
        valve, minutes, valves_open, pressure = queue.pop(0)
        max_pressure = max(pressure, max_pressure)

        for n in all_valves:
            if n != valve and n not in valves_open:
                t = time_to[valve][n]
                next_minutes = minutes - t
                
                if next_minutes > 0:
                    queue.append((n, next_minutes, valves_open.union({valve}), pressure + (flow_rates[n] * next_minutes)))

    return max_pressure

assert 1651 == part_one(test_input_str)

print("part one:", part_one(puzzle_input_str))
part one: 1754
%%time

from itertools import permutations

def solve(me, elephant, valves_open, pressure, all_valves, flow_rates, time_to):
    me_v      , me_t       = me
    elephant_v, elephant_t = elephant    
    combs = permutations([v for v in all_valves if v not in valves_open and v != me_v and v != elephant_v], 2)
    solutions = [pressure]
    
    for me_vnext, elephant_vnext in combs:
        me_tnext       = me_t       - time_to[me_v][me_vnext]
        elephant_tnext = elephant_t - time_to[elephant_v][elephant_vnext]

        me_pnext = flow_rates[me_vnext] * me_tnext
        elephant_pnext = flow_rates[elephant_vnext] * elephant_tnext

        if me_tnext < 0 and elephant_tnext < 0:
            break

        if me_tnext < 0:
            me_tnext = me_t
            me_vnext = me_v
            me_pnext = 0

        if elephant_tnext < 0:
            elephant_tnext = elephant_t
            elephant_vnext = elephant_v
            elephant_pnext = 0

        pressure_next = pressure + me_pnext + elephant_pnext

        solutions.append(
            solve(
                (me_vnext, me_tnext),
                (elephant_vnext, elephant_tnext),
                valves_open.union({me_vnext, elephant_vnext}),
                pressure_next,
                all_valves, flow_rates, time_to
            )
        )
        
    return max(solutions)
   
        
        
def part_two(input_str):
    valves = [parse_line(line) for line in input_str.splitlines()]
    flow_rates = {v[0]:v[1] for v in valves}
    neighbours = {v[0]:v[2] for v in valves}
    all_valves = {v[0] for v in valves if flow_rates[v[0]] != 0}
    time_to = build_distances_lookup(neighbours)
    
    max_pressure = 0
    
    return solve(("AA",26), ("AA", 26), set(), 0, all_valves, flow_rates, time_to)


assert 1707 == part_two(test_input_str)

print("part_two:", part_two(puzzle_input_str))
part_two: 2474
CPU times: user 9h 32min 1s, sys: 3.17 s, total: 9h 32min 4s
Wall time: 9h 32min 15s
# ABORTED BFS VERSION - this is my first attempt, it eats up all available RAM. 0/10, would not recommend. 
# I've leaving this here because I usually *sort of* polish my solutions up, and I figured it might be interesting to see
# not only an unpolished work-in-progress one with debug prints and such, but a completely failed one :)

from itertools import permutations


def part_two(input_str):
    valves = [parse_line(line) for line in input_str.splitlines()]
    flow_rates = {v[0]:v[1] for v in valves}
    neighbours = {v[0]:v[2] for v in valves}
    all_valves = {v[0] for v in valves if flow_rates[v[0]] != 0}
    time_to = build_distances_lookup(neighbours)
    
    max_pressure = 0
    queue = [(("AA", 26), ("AA", 26), set(), 0)]

    print(len(all_valves))
    n = 0
    while queue:
        me, elephant, valves_open, pressure = queue.pop(0)

        # CAREFUL!!! I may have fucked this up
        max_pressure = max(pressure, max_pressure)
        
        me_v      , me_t       = me
        elephant_v, elephant_t = elephant
        print(len([v for v in all_valves if v not in valves_open and v != me_v and v != elephant_v]))
        combs = list(permutations([v for v in all_valves if v not in valves_open and v != me_v and v != elephant_v], 2))
        #print(valves_open, f"({me_v}, {elephant_v})", len(combs), me_t, elephant_t)
        for me_vnext, elephant_vnext in combs:
            me_tnext       = me_t       - time_to[me_v][me_vnext]
            elephant_tnext = elephant_t - time_to[elephant_v][elephant_vnext]

            me_pnext = flow_rates[me_vnext] * me_tnext
            elephant_pnext = flow_rates[elephant_vnext] * elephant_tnext
            
            if me_tnext < 0 and elephant_tnext < 0:
                continue

            if me_tnext < 0:
                me_tnext = me_t
                me_vnext = me_v
                me_pnext = 0

            if elephant_tnext < 0:
                elephant_tnext = elephant_t
                elephant_vnext = elephant_v
                elephant_pnext = 0

            pressure_next = pressure + me_pnext + elephant_pnext
    
            queue.append((
                (me_vnext, me_tnext),
                (elephant_vnext, elephant_tnext),
                valves_open.union({me_vnext, elephant_vnext}),
                pressure_next))
        
        #print(n, len(valves_open), len(queue), max_pressure)
        n += 1
#         if n == 1:
#             from pprint import pprint
#             pprint(queue)
#             print()
        if n == 2:
#             from pprint import pprint
#             pprint(queue)
#             print()
            break
#         if n == 3:
#             from pprint import pprint
#             pprint(queue)
#             #break
            
    return max_pressure


part_two(test_input_str)
# 1657 too low
#print("part two:", part_two(puzzle_input_str))
6
6
4
792