expeditaet
Version:
Advent of Code Solutions
192 lines (165 loc) • 5.37 kB
text/typescript
import {
Direction,
GridGraph,
GridGraphNode,
task,
type ToString,
} from '@alexaegis/advent-of-code-lib';
import packageJson from '../package.json';
import { parse, weighter } from './parse.js';
export const findPartition = <
T extends ToString = string,
N extends GridGraphNode<T> = GridGraphNode<T>,
>(
partitions: Map<N, number>[],
node: N,
): Map<N, number> => {
const partition = partitions.find((p) => p.has(node));
if (!partition) {
throw new Error('No partition found for node');
}
return partition;
};
const enclosedPartitions = <
T extends ToString = string,
N extends GridGraphNode<T> = GridGraphNode<T>,
>(
partitions: Map<N, number>[],
encloser: Map<N, number>,
): Map<N, number>[] => {
const partitionsEnclosedByLoop = partitions.filter(
(partition) =>
partition !== encloser &&
partition.keyArray().every(
(pn) =>
pn.neighbours.size >= 4 && // Not an edge node
pn.neighbours
.valueArray()
.every((pnn) => encloser.has(pnn.to) || partition.has(pnn.to)),
),
);
return partitionsEnclosedByLoop;
};
export const partitionGraph = <
T extends ToString = string,
N extends GridGraphNode<T> = GridGraphNode<T>,
>(
graph: GridGraph<T, N>,
foundPartitions: Map<N, number>[],
): Map<N, number>[] => {
const nonPartitionedNodes = new Set(graph.nodes.values());
for (const foundPartition of foundPartitions) {
for (const [foundNode] of foundPartition) {
nonPartitionedNodes.delete(foundNode);
}
}
while (nonPartitionedNodes.size > 0) {
const nodeNotInTheLoop = nonPartitionedNodes.values().next().value as N;
const nextPartition = graph.flood({ start: nodeNotInTheLoop });
for (const [foundNode] of nextPartition) {
nonPartitionedNodes.delete(foundNode);
}
foundPartitions.push(nextPartition);
}
return foundPartitions;
};
export interface GraphLoopPartitions<
T extends ToString = string,
N extends GridGraphNode<T> = GridGraphNode<T>,
> {
loop: Set<N>;
/**
* empty if the loop has no nodes inside, or the loop is not closed
*/
inside: Set<N>;
/**
* empty if the loop has no nodes outside, or the loop is not closed
*/
outside: Set<N>;
}
export const partitionIntoTwoFromLoop = <
T extends ToString = string,
N extends GridGraphNode<T> = GridGraphNode<T>,
>(
graph: GridGraph<T, N>,
loop: Map<N, number>,
): GraphLoopPartitions<T, N> => {
const nonPartitionedNodes = new Set(graph.nodes.values());
for (const [foundNode] of loop) {
nonPartitionedNodes.delete(foundNode);
}
const foundPartitions: Map<N, number>[] = [];
while (nonPartitionedNodes.size > 0) {
const nodeNotInTheLoop = nonPartitionedNodes.values().next().value as N;
const nextPartition = graph.flood({
start: nodeNotInTheLoop,
currentPathWeighter: (a, b) =>
loop.has(a) || loop.has(b) ? Number.POSITIVE_INFINITY : 1,
});
for (const [foundNode] of nextPartition) {
nonPartitionedNodes.delete(foundNode);
}
foundPartitions.push(nextPartition);
}
const insides = enclosedPartitions<T, N>(foundPartitions, loop);
const outside = foundPartitions.find((p) => !insides.includes(p));
return {
loop: new Set(loop.keys()),
inside: new Set(insides.flatMap((i) => i.keyArray())),
outside: new Set(outside?.keys()),
};
};
export const p2 = (input: string): number => {
const originalGraph = parse(input);
const liminalNodes = new Set<GridGraphNode>();
// Expanding graph to address liminal space
const graph = new GridGraph<string>();
for (const ogNode of originalGraph.nodeValues) {
for (const { from, to, weight, direction } of Direction.allDirections.map((direction) => ({
...ogNode.neighbours.get(direction),
from: ogNode,
to: originalGraph.getNode(ogNode.coordinate.add(direction)),
direction,
}))) {
if (to === undefined) {
continue;
}
let newNodeSymbol = '.';
if (from.value === '.' && to.value === '.') {
newNodeSymbol = '.';
} else if (weight && weight < Number.POSITIVE_INFINITY) {
newNodeSymbol = direction.isHorizonal() ? '-' : '|';
}
const fromDoubled = from.coordinate.add(from.coordinate);
const toDoubled = to.coordinate.add(to.coordinate);
const middle = fromDoubled.middle(toDoubled);
const newFrom = graph.nodes.getOrAdd(
fromDoubled.toString(),
(_n) => new GridGraphNode(fromDoubled, from.value),
);
const newMiddle = graph.nodes.getOrAdd(
middle.toString(),
(_n) => new GridGraphNode(middle, newNodeSymbol),
);
liminalNodes.add(newMiddle);
const newTo = graph.nodes.getOrAdd(
toDoubled.toString(),
(_n) => new GridGraphNode(toDoubled, to.value),
);
newFrom.attachNeightbours(graph, Direction.cardinalDirections, weighter);
newMiddle.attachNeightbours(graph, Direction.cardinalDirections, weighter);
newTo.attachNeightbours(graph, Direction.cardinalDirections, weighter);
}
}
const animalStart = graph.findNode((node) => node.value === 'S');
if (!animalStart) {
throw new Error('no starting position for the animal!');
}
const pathLoop = graph.flood({ start: animalStart });
const { inside, outside } = partitionIntoTwoFromLoop(graph, pathLoop);
if (process.env['RUN']) {
graph.print((n) => (inside.has(n) ? 'I' : outside.has(n) ? 'O' : n.toString()));
}
return inside.valueArray().filter((node) => !liminalNodes.has(node)).length ?? -1;
};
await task(p2, packageJson.aoc); // 435 ~1.4s