const fs = require('fs/promises'); const sum = (arr) => arr.reduce((total, num) => total+num, 0); const arrayOfLength = n => Array.from(Array(n).keys()).map(() => {}); const createNode = (str) => { const stats = str.match(/Valve ([A-Z]+) has flow rate=([\d]+); tunnels? leads? to valves? (.*)$/); if(!stats) { console.log("Bad line? : ", str); return; } const id = stats[1]; const rate = stats[2]; const edges = stats[3].split(', '); return { id, rate, edges, }; }; const main = async () => { const input = (await fs.readFile("./input.txt", 'utf8')).split("\n").slice(0, -1).sort(); const nodesById = {}; for(const row of input) { const node = createNode(row); if(!node) continue; nodesById[node.id] = node; } const nodesByRate = Object.values(nodesById).sort((a, b) => b.rate - a.rate).filter(n => n.rate > 0); const findEachOptimalPath = (node, queue = [], visited = {}, path = []) => { visited[node.id] = path; for(const edge of node.edges) { const neighbor = nodesById[edge]; if(typeof visited[neighbor.id] === "undefined") { const nextPath = [...path, neighbor]; queue.push(() => findEachOptimalPath(neighbor, queue, visited, nextPath)); } }; if(queue.length) { return queue.shift()(); } return visited; }; const valvePaths = Object.values(nodesById).reduce((possibilities, node) => { possibilities[node.id] = findEachOptimalPath(node); return possibilities; }, {}); let best = 0; const bestPerTime = []; let count = 0; const perfectPlan = (node1, node2, target1, target2, value = 0, targeted = {"AA": true}, time = 26) => { if(time < 1) { return; } count++; if(!(count % 2000000)) { console.log("Evaluated ", count, "possible minutes"); } let acted1 = false; let acted2 = false; if(target1 === node1) { target1 = undefined; acted1 = true; value = value + (time - 1) * node1.rate; } if(target2 === node2) { target2 = undefined; acted2 = true; value = value + (time - 1) * node2.rate; } if(value > best) { best = value; console.log(`${best} is better!`); } if(!bestPerTime[time] || bestPerTime[time] <= value) { bestPerTime[time] = value; } //1.5 seems to work. Higher values work faster, but risk missing the best path const lowBar = bestPerTime[Math.floor(time + time/1.5)]; if(lowBar && lowBar > value) { return; } const paths1 = valvePaths[node1.id]; const paths2 = valvePaths[node2.id]; const possibleNodes = nodesByRate.filter(n => !targeted[n.id]); for(const next1 of possibleNodes) { for(const next2 of possibleNodes) { if(next1 === next2) { continue; } const n1 = acted1 ? node1 : target1 ? paths1[target1.id][0] : paths1[next1.id][0]; const n2 = acted2 ? node2 : target2 ? paths2[target2.id][0] : paths2[next2.id][0]; const t1 = acted1 ? null : target1 || next1; const t2 = acted2 ? null : target2 || next2; if(t2 && t1 && paths1[t2.id].length < paths2[t2.id].length && paths2[t1.id].length < paths2[t2.id].length) { continue; } const newTargeted = {...targeted}; if(t1) { newTargeted[t1.id] = true; } if(t2) { newTargeted[t2.id] = true; } perfectPlan(n1, n2, t1, t2, value, {...newTargeted}, time - 1); if(target2 || acted2) break; } if(target1 || acted1) break; } }; perfectPlan(nodesById["AA"], nodesById["AA"]); console.log("Answer is: ", best); } main();