expeditaet
Version:
Advent of Code Solutions
228 lines (201 loc) • 7.02 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { task } from '@alexaegis/advent-of-code-lib';
import packageJson from '../package.json';
import { parse, type Valve } from './parse.function.js';
const OPEN = 'open';
const WAIT = 'wait';
type PathSegment = Valve | typeof OPEN | typeof WAIT;
type Path = PathSegment[];
interface ValveNode {
path: Path;
pressureReleasedSoFar: number;
openValves: Valve[];
// edgesVisited: string[];
/**
* Not worth going because in that direction everything is either opened
* or not worth opening
*/
forbiddenEdges: string[];
forbiddenValves: string[];
}
const normalizeOption = (key: string, valveMap: Map<string, Valve>): PathSegment => {
return key === OPEN || key === WAIT ? key : valveMap.get(key)!;
};
const isValveSegment = (segment: PathSegment): segment is Valve => {
return typeof segment === 'object';
};
const serializePath = (path: Path): string => {
return path.map((segment) => (typeof segment === 'object' ? segment.name : segment)).join(',');
};
/***
* Backtracking algorithm is needed
*/
export const p1 = (input: string): number => {
const valves = parse(input);
const valveMap = new Map<string, Valve>();
for (const valve of valves) {
valveMap.set(valve.name, valve);
}
const valvesWorthOpening = valves.filter((valve) => valve.flowRate > 0);
/**
* This map contains edges where the keys are paths like "AA,open,CC,II,wait"
* To get the previous one, pop one off the path and calculate it's key
*
* To get destinations calculate it from the last path element
*
* Once the graph is filled filter out all paths that are marked as end
*
* And there's some other metadata specific to the task,
*
* graphSpace cache
*/
const solutionGraph = new Map<string, ValveNode>();
// Start at "AA"
/**
* solutionSpace cache
*/
let currentSolution: ValveNode | undefined = {
path: [valveMap.get('AA')!],
pressureReleasedSoFar: 0,
openValves: [],
forbiddenEdges: [],
forbiddenValves: [],
};
solutionGraph.set('AA', currentSolution);
const _expectedPathForExample =
'AA,DD,open,CC,BB,open,AA,II,JJ,open,II,AA,DD,EE,FF,GG,HH,open,GG,FF,EE,open,DD,CC,open,wait,wait,wait...';
let i = 0;
console.log('ohboy');
while (currentSolution) {
const currentValve = currentSolution.path.findLast(isValveSegment)!;
const currentPath = currentSolution.path;
const currentPathSerialized = serializePath(currentSolution.path);
const currentlyForbiddenEdges = currentSolution.forbiddenEdges;
const currentlyForbiddenValves = currentSolution.forbiddenValves;
i++;
/**
* &&
currentPathSerialized.startsWith('AA,DD,open,CC,BB,open,AA,II,JJ,open,II,AA')
*/
if (
i % 1 === 0 &&
currentPathSerialized.startsWith('AA,DD,open,CC,BB,open,AA,II,JJ,open,II,AA')
) {
console.log(
i,
'Checking solution',
currentPathSerialized,
'time passed',
currentSolution.path.length,
'openValves',
currentSolution.openValves.map((v) => v.name).join(','),
'forbiddenEdges',
currentSolution.forbiddenEdges.join('; '),
'pressure released',
currentSolution.pressureReleasedSoFar,
);
}
const nextOptions: string[] = [];
// It's its worth opening and wasn't already opened this round, or this path wasnt tried
if (
currentValve !== 'open' &&
currentValve !== 'wait' &&
currentValve.flowRate > 0 &&
!currentSolution.openValves.contains(currentValve) &&
!solutionGraph.has(`${currentPathSerialized},${OPEN}`)
) {
nextOptions.push(OPEN);
}
const nextForbiddenValves = [...currentlyForbiddenValves];
if (currentSolution.openValves.length === valvesWorthOpening.length) {
// Everything is open, the only thing left is waiting
nextOptions.push(WAIT);
} else if (currentValve !== 'open' && currentValve !== 'wait') {
const notCoveredValves = currentValve.leadsTo.filterMap((valveKey) => {
const wouldBeSolution = `${currentPathSerialized},${valveKey}`;
const graphAlreadyCoversSolution = solutionGraph.has(wouldBeSolution);
return !currentlyForbiddenValves.includes(valveKey) && !graphAlreadyCoversSolution
? valveKey
: undefined;
});
const strictNotCoveredEdges = notCoveredValves.filter((valveKey) => {
const wouldBeEdge = `${currentValve.name},${valveKey}`;
return !currentPathSerialized.includes(wouldBeEdge);
});
if (strictNotCoveredEdges.length > 0) {
nextOptions.push(...strictNotCoveredEdges);
} else if (notCoveredValves.length > 0) {
nextOptions.push(...notCoveredValves);
} else {
nextForbiddenValves.push(currentValve.name);
}
}
if (currentPath.length > 30) {
nextOptions.clear();
}
let backtrack = false;
if (nextOptions.length > 0) {
// If there is somewhere to go, try the first option
const nextOption = normalizeOption(nextOptions[0]!, valveMap);
const nextPath = [...currentPath, nextOption];
const nextPathSerialized = serializePath(nextPath);
//if (isValveSegment(nextOption)) {
// const nextEdge = `${currentValve.name},${nextOption.name}`;
// // nextAlreadyWalked.push(nextEdge);
//}
// release pressure
const releasedThisRound = currentSolution.openValves
.map((valve) => valve.flowRate)
.sum();
const openValves = [...currentSolution.openValves];
if (
currentValve !== 'open' &&
currentValve !== 'wait' &&
nextOption === OPEN &&
!openValves.includes(currentValve)
) {
openValves.push(currentValve);
}
if (solutionGraph.has(nextPathSerialized)) {
backtrack = true;
} else {
const nextSolution: ValveNode = {
path: nextPath,
openValves,
pressureReleasedSoFar:
currentSolution.pressureReleasedSoFar + releasedThisRound,
forbiddenEdges: currentlyForbiddenEdges,
forbiddenValves: nextForbiddenValves,
};
solutionGraph.set(nextPathSerialized, nextSolution);
currentSolution = nextSolution;
}
} else {
backtrack = true;
}
if (backtrack) {
// backtrack
const prev = currentSolution.path.clone();
prev.pop();
const prevPathKey = serializePath(prev);
// If we try to backtrack from the starting, 1 long path, we get an
// empty key, currentNode gets set to undefined and the while loop breaks
currentSolution = solutionGraph.get(prevPathKey);
}
}
const finishedSolutions = solutionGraph
.valueArray()
.filter((solution) => solution.path.length > 30)
.sort((a, b) => b.pressureReleasedSoFar - a.pressureReleasedSoFar);
console.log('finishedSolutions', finishedSolutions.length);
const solutionWithMostReleasedPressure = finishedSolutions[0];
const serializedSolutionPath = serializePath(solutionWithMostReleasedPressure!.path);
console.log(
'solutionWithMostReleasedPressure',
serializedSolutionPath,
'is it correct for the exmaple?',
serializedSolutionPath.startsWith(_expectedPathForExample),
);
return finishedSolutions[0]!.pressureReleasedSoFar;
};
await task(p1, packageJson.aoc, 'example.1.txt'); // 0 ~0ms