aboutsummaryrefslogtreecommitdiffstats
path: root/day16/solution.nim
blob: ae93568b9437d297af35939eeed3235ca730acdf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import strutils
import sequtils
import re
import tables
import strformat
import algorithm
import sets

type
  NodeData = tuple
    pressure: int
    neighbors: seq[string]

  Node = tuple
    id: string
    data: NodeData

  NodeGraph = Table[string, NodeData]

  ExtendedNodeData = tuple
    pressure: int
    neighbors: Table[string, int]

  ExtendedNodeGraph = Table[string, ExtendedNodeData]

proc parseLine(line: string): Node =
  let tokens = line.replace("Valve ")
                   .replace("has flow rate=")
                   .replace(re"; tunnels? leads? to valves?")
                   .replace(", ", " ")
                   .split(" ")
  (tokens[0], (tokens[1].parseInt(), @tokens[2..^1]))

proc parseFile(content: string): NodeGraph =
  let lines = content.strip().splitLines()
  let nodes = map(lines, parseLine)

  for node in nodes:
    result[node.id] = node.data

proc bfs(input: NodeGraph, src: string): Table[string, int] =
  var q = @[(src, 0)]
  result[src] = 0

  while q.len() > 0:
    let (curr, pathLen) = q[0]
    q.delete(0)

    for neighbor in input[curr].neighbors:
      if result.contains(neighbor):
        continue
      result[neighbor] = pathLen+1
      q.add((neighbor, pathLen+1))

proc setup(input: NodeGraph): ExtendedNodeGraph =
  for node in input.keys():
    # Exclude all 0 pressure nodes except the entry node "AA"
    if (input[node].pressure == 0 and node != "AA"):
      continue
    let allConnected = bfs(input, node)
    var filtConnected: Table[string, int]
    for conn in allConnected.keys():
      if input[conn].pressure == 0:
        continue
      filtConnected[conn] = allConnected[conn]

    result[node] = (input[node].pressure, filtConnected)
    # echo fmt"{node} : {result[node]}"

type Agent = tuple
  valv: string
  time: int

# Nodes with their neighbors
var Graph: ExtendedNodeGraph
# Node names
var nodes = newSeq[string]()

proc playing(agent: Agent, time: int): bool =
  agent.time == time

proc agentScores(agent: Agent, time: int): int =
  Graph[agent.valv].pressure * (time-1)

# type ValveSet = uint64
# proc incl(set: var ValveSet, valve: uint64): void =
#   set = set or valve

var part2 = false
proc dfs(visited: HashSet[string], time: int, human, elephant: Agent): int =
  # echo fmt"time: {time}, human: {human}, elephant: {elephant}"
  let remainingValves = nodes.len() - visited.len() 
  if time <= 1 or remainingValves == 0 or (part2 and human.valv == elephant.valv and remainingValves > 1):
    return 0

  let
    neighborsHuman = Graph[human.valv].neighbors
    neighborsElephant = Graph[elephant.valv].neighbors
  var
    # check for race
    humanScore = if human.valv notin visited: human.agentScores(time) else: 0
    elephantScore = if elephant.valv notin visited: elephant.agentScores(time) else: 0

  var bestNextScore = 0
  if human.playing(time) and elephant.playing(time):
    for nextHuman in nodes:
      for nextElephant in nodes:
        if nextHuman in visited or nextElephant in visited:
          continue
        let
          nextTimeHuman = time - 1 - neighborsHuman[nextHuman]
          nextTimeElephant = time - 1 - neighborsElephant[nextElephant]
          nextTime = max(nextTimeHuman, nextTimeElephant)
          nextScore = dfs(union(visited, toHashSet(@[elephant.valv, human.valv])), nextTime, (nextHuman, nextTimeHuman), (nextElephant, nextTimeElephant))
        bestNextScore = max(bestNextScore, nextScore)

    # elephant keeps his score only if he isn't in the same valve as human
    elephantScore = (if human != elephant: elephantScore else: 0)
    return bestNextScore + humanScore + elephantScore

  if human.playing(time):
    for nextHuman in nodes:
      if nextHuman in visited:
        continue
      let
        nextTimeHuman = time - 1 - neighborsHuman[nextHuman]
        nextTime = max(nextTimeHuman, elephant.time)
        nextScore = dfs(union(visited, toHashSet(@[human.valv])), nextTime, (nextHuman, nextTimeHuman), elephant)
      bestNextScore = max(bestNextScore, nextScore)

    # Check for race
    return bestNextScore + humanScore

  if elephant.playing(time):
    for nextElephant in nodes:
      if nextElephant in visited:
        continue
      let
        nextTimeElephant = time - 1 - neighborsElephant[nextElephant]
        nextTime = max(human.time, nextTimeElephant)
        nextScore = dfs(union(visited, toHashSet(@[elephant.valv])), nextTime, human, (nextElephant, nextTimeElephant))
      bestNextScore = max(bestNextScore, nextScore)

    # Check for race
    return bestNextScore + elephantScore
  
  assert(false)

proc solve(initDelays: Table[string, int], time: int): int =

  if not part2:
    for human in nodes:
      let
        humanStartTime = time - initDelays[human]
        startTime = humanStartTime
      result = max(result, dfs(initHashSet[string](), startTime, (human, humanStartTime), (human, -1)))
  else:
    for i, human in nodes[0 ..< ^1]:
      for elephant in nodes[i+1 .. ^1]:
        let
          elephantStartTime = time - initDelays[elephant]
          humanStartTime = time - initDelays[human]
          startTime = max(elephantStartTime, humanStartTime)
        result = max(result, dfs(initHashSet[string](), startTime, (human, humanStartTime), (elephant, elephantStartTime)))

let content = readFile("./example.txt")

let input = parseFile(content)
Graph = setup(input)
# Table of delays to get from starting node "AA" to current
let initDelays = Graph["AA"].neighbors
Graph.del("AA")

for node in Graph.keys():
  nodes.add(node)

echo solve(initDelays, 30)
part2 = true
echo solve(initDelays, 26)