Advent of Code 2022 day 16

I’ve taken to separating the two parts into different Python files now, since the code is growing in complexity. Part 2 improved upon the length of time set out by part 1 to work out the distances between the valves. However, despite this, it takes an age to run. Used permutations for part 2. I might have been able to get away with combinations, but I wanted to be safe.

with open("16_test.txt") as f:
# with open("16.txt") as f:
    data = f.read()

def get_id(line):
    return line.split("Valve ")[1].split(" ")[0]

class Valve:
    def __init__(self, data):
        self.id = get_id(data)
        self.flow_rate = self.get_flow_rate(data)
        self.neighbours = self.get_neighbours(data)

    def get_flow_rate(self, line):
        return int(line.split("rate=")[1].split(";")[0])

    def get_neighbours(self, line):
        if "valves " in line:
            return line.split("valves ")[1].split(", ")
        return [line.split("leads to valve ")[1]]

valves = {get_id(line): Valve(line) for line in data.splitlines()}

def shortest(paths):
    if len(paths) > 0:
        return sorted(paths, key=lambda x: len(x))[0]
    return None

def get_path_to(start, end, path):
    if end == start:
        return []
    elif end in valves[start].neighbours:
        return path+[end]
    elif len(path) > 1 and start in path[:-1]:
        return False
    paths = []
    for x in valves[start].neighbours:
        if possible_path:=get_path_to(x, end, path+[x]):
            paths.append(possible_path)
    return shortest(paths)

def get_distances(valves):
    distances = {}
    for start in valves:
        for end in valves:
            if start == end:
                distances[(start, end)] = 0
            elif end in valves[start].neighbours:
                distances[(start, end)] = 1
            else:
                for x in valves[start].neighbours:
                    if end in valves[x].neighbours:
                        distances[(start, end)] = 2
                        break
                else:
                    distances[(start, end)] = len(
                        get_path_to(start, end, [])
                    )
    return distances

def answer(rounds, current, unopened, distances):
    if rounds <= 0:
        return 0
    score = 0
    for candidate in unopened:
        new_unopened = [x for x in unopened if x is not candidate]
        new_rounds = rounds-1-distances[(current.id, candidate.id)]
        if new_rounds <= 0:
            continue
        score = max(
            score,
            new_rounds*candidate.flow_rate+answer(
                new_rounds,
                candidate,
                new_unopened,
                distances
            )
        )
    return score

distances = get_distances(valves)
current = valves["AA"]
unopened = {valve for valve in valves.values() if valve.flow_rate > 0}

print(answer(30, current, unopened, distances))

# Part 2
from itertools import permutations

# with open("16_test.txt") as f:
with open("16.txt") as f:
    data = f.read()

def get_id(line):
    return line.split("Valve ")[1].split(" ")[0]

class Valve:
    def __init__(self, data):
        self.id = get_id(data)
        self.flow_rate = self.get_flow_rate(data)
        self.neighbours = self.get_neighbours(data)

    def __str__(self):
        return f"{self.id}"

    def __repr__(self):
        return f"{self.id}"

    def get_flow_rate(self, line):
        return int(line.split("rate=")[1].split(";")[0])

    def get_neighbours(self, line):
        if "valves " in line:
            return line.split("valves ")[1].split(", ")
        return [line.split("leads to valve ")[1]]

valves = {get_id(line): Valve(line) for line in data.splitlines()}

for valve in valves.values():
    valve.neighbours = [valves[x] for x in valve.neighbours]

def get_distance(start, end, distances, used=None):
    if used is None:
        used = []

    if distances[end][start] is not None:
        return distances[end][start]
    elif start == end:
        return 0
    elif end in start.neighbours:
        return 1

    min_distance = 10**6
    if start in used:
        return min_distance
    used.append(start)
    for x in start.neighbours:
        min_distance = min(
            min_distance,
            get_distance(x, end, distances, used=used)+1
        )
    return min_distance

def get_distances(valves):
    distances = {x: {y: None for y in valves} for x in valves}
    for x in valves:
        for y in valves:
            if distances[y][x] is not None:
                continue
            elif x == y:
                distances[y][x] = 0
            elif y in x.neighbours:
                distances[y][x] = 1
            else:
                distances[y][x] = get_distance(x, y, distances)
    return distances

distances = get_distances(valves.values())

def solve(round_a, round_b, current_a, current_b, unopened, distances):
    if round_a <= 0:
        return 0
    if round_b <= 0:
        return 0

    score = 0
    if len(unopened) == 0:
        return score
    elif len(unopened) == 1:
        new_score = 0
        candidate_a = unopened[0]
        new_round_a = round_a - distances[candidate_a][current_a] - 1
        if  new_round_a > 0:
            new_score += candidate_a.flow_rate*new_round_a
        return max(score, new_score)
    elif len(unopened) > 1:
        for candidate_a, candidate_b in permutations(unopened, 2):
            new_score = 0
            new_unopened = [
                x for x in unopened
                if x is not candidate_a and x is not candidate_b
            ]
            new_round_a = round_a - distances[candidate_a][current_a] - 1
            new_round_b = round_b - distances[candidate_b][current_b] - 1
            if new_round_a > 0:
                new_score += candidate_a.flow_rate*new_round_a
            if new_round_b > 0:
                new_score += candidate_b.flow_rate*new_round_b
            score = max(
                score,
                new_score + solve(
                    new_round_a,
                    new_round_b,
                    candidate_a,
                    candidate_b,
                    new_unopened,
                    distances
                )
            )
    return score

unopened = [x for x in valves.values() if x.flow_rate > 0]
print(solve(26, 26, valves["AA"], valves["AA"], unopened, distances))