|
| 1 | +import { |
| 2 | + addXY, |
| 3 | + Direction, |
| 4 | + flipDir, |
| 5 | + getNeighbors, |
| 6 | + NEIGHBOR_XY_4WAY, |
| 7 | + toGrid, |
| 8 | + toXY, |
| 9 | + XY, |
| 10 | +} from '../util' |
| 11 | + |
| 12 | +const parseInput = (input: string) => { |
| 13 | + let startXY: XY | undefined |
| 14 | + let endXY: XY | undefined |
| 15 | + const map = input |
| 16 | + .trim() |
| 17 | + .split('\n') |
| 18 | + .map((line, y) => |
| 19 | + line.split('').map((v, x) => { |
| 20 | + if (v === 'S') { |
| 21 | + startXY = [x, y] |
| 22 | + return 0 |
| 23 | + } |
| 24 | + if (v === 'E') { |
| 25 | + endXY = [x, y] |
| 26 | + return 0 |
| 27 | + } |
| 28 | + return v === '.' ? 0 : 1 |
| 29 | + }) |
| 30 | + ) |
| 31 | + if (!startXY || !endXY) throw 'start/end not found!' |
| 32 | + return { map, startXY, endXY } |
| 33 | +} |
| 34 | + |
| 35 | +const buildNodeGraph = (map: number[][], startGrid: string, endGrid: string) => { |
| 36 | + const nodeGridList: Set<string> = new Set([startGrid, endGrid]) |
| 37 | + for (let y = 1; y < map.length - 1; y++) { |
| 38 | + const row = map[y] |
| 39 | + for (let x = 1; x < row.length - 1; x++) { |
| 40 | + if (row[x] === 1) continue |
| 41 | + const openNeighbors = getNeighbors(x, y).filter(([nx, ny]) => map[ny][nx] === 0) |
| 42 | + if (openNeighbors.length > 2) nodeGridList.add(toGrid(x, y)) |
| 43 | + } |
| 44 | + } |
| 45 | + const nodeGraph: NodeGraph = new Map() |
| 46 | + nodeGridList.forEach((iGrid) => nodeGraph.set(iGrid, new Map())) |
| 47 | + const openNodes: Set<string> = new Set([startGrid]) |
| 48 | + const closedNodes: Set<string> = new Set() |
| 49 | + while (openNodes.size > 0) { |
| 50 | + const fromNode = [...openNodes][0] |
| 51 | + const fromNodeXY = toXY(fromNode) |
| 52 | + const fromNetworkNode = nodeGraph.get(fromNode)! |
| 53 | + const exploredDirs = [...fromNetworkNode].map(([, { startDir }]) => startDir) |
| 54 | + openNodes.delete(fromNode) |
| 55 | + closedNodes.add(fromNode) |
| 56 | + for (let startDir = 0; startDir < 4; startDir++) { |
| 57 | + if (exploredDirs.includes(startDir)) continue |
| 58 | + const firstStepXY = addXY(fromNodeXY, NEIGHBOR_XY_4WAY[startDir]) |
| 59 | + const firstStepTile = map[firstStepXY[1]][firstStepXY[0]] |
| 60 | + if (firstStepTile === 1) continue |
| 61 | + let currentDir = startDir |
| 62 | + let currentXY = firstStepXY |
| 63 | + let steps = 1 |
| 64 | + let turns = 0 |
| 65 | + let intersectionFound = false |
| 66 | + const grids = [fromNode, toGrid(...firstStepXY)] |
| 67 | + while (!intersectionFound) { |
| 68 | + let nextStepFound = false |
| 69 | + for (let turn = -1; turn <= 1; turn++) { |
| 70 | + const nextDir = (currentDir + 4 + turn) % 4 |
| 71 | + const nextStepXY = addXY(currentXY, NEIGHBOR_XY_4WAY[nextDir]) |
| 72 | + const nextStepGrid = toGrid(...nextStepXY) |
| 73 | + if (nodeGraph.has(nextStepGrid)) { |
| 74 | + if (nextStepGrid === fromNode) break |
| 75 | + steps++ |
| 76 | + if (turn !== 0) turns++ |
| 77 | + const cost = turns * 1000 + steps |
| 78 | + grids.push(nextStepGrid) |
| 79 | + fromNetworkNode.set(`${nextStepGrid}~${startDir}`, { |
| 80 | + startDir, |
| 81 | + cost, |
| 82 | + endDir: nextDir, |
| 83 | + grids, |
| 84 | + }) |
| 85 | + intersectionFound = true |
| 86 | + if (!closedNodes.has(nextStepGrid)) openNodes.add(nextStepGrid) |
| 87 | + break |
| 88 | + } |
| 89 | + const nextStepTile = map[nextStepXY[1]][nextStepXY[0]] |
| 90 | + if (nextStepTile === 1) continue |
| 91 | + nextStepFound = true |
| 92 | + steps++ |
| 93 | + if (turn !== 0) turns++ |
| 94 | + currentDir = nextDir |
| 95 | + currentXY = nextStepXY |
| 96 | + grids.push(nextStepGrid) |
| 97 | + break |
| 98 | + } |
| 99 | + if (!nextStepFound) { |
| 100 | + // dead end |
| 101 | + break |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + while (true) { |
| 107 | + let nodesRemoved = 0 |
| 108 | + nodeGraph.forEach((node, nodeGrid) => { |
| 109 | + if (nodeGrid === startGrid || nodeGrid === endGrid) return |
| 110 | + if (node.size === 1) { |
| 111 | + nodeGraph.forEach((compareNode) => { |
| 112 | + compareNode.forEach((_, hash) => { |
| 113 | + if (hash.split('~')[0] === nodeGrid) compareNode.delete(hash) |
| 114 | + }) |
| 115 | + }) |
| 116 | + nodeGraph.delete(nodeGrid) |
| 117 | + nodesRemoved++ |
| 118 | + } |
| 119 | + }) |
| 120 | + if (nodesRemoved === 0) break |
| 121 | + } |
| 122 | + return nodeGraph |
| 123 | +} |
| 124 | + |
| 125 | +type PathNode = { |
| 126 | + grid: string |
| 127 | + hash: string |
| 128 | + dir: number |
| 129 | + score: number |
| 130 | + grids: string[] |
| 131 | +} |
| 132 | + |
| 133 | +const findPath = (map: number[][], start: XY, end: XY) => { |
| 134 | + const startGrid = toGrid(...start) |
| 135 | + const endGrid = toGrid(...end) |
| 136 | + const network = buildNodeGraph(map, startGrid, endGrid) |
| 137 | + let current: PathNode = { |
| 138 | + grid: startGrid, |
| 139 | + hash: `${startGrid}:${Direction.right}`, |
| 140 | + dir: Direction.right, |
| 141 | + score: 0, |
| 142 | + grids: [startGrid], |
| 143 | + } |
| 144 | + const openTiles: Map<string, PathNode> = new Map([[current.hash, current]]) |
| 145 | + const bestHashScores: Map<string, [number, Set<string>]> = new Map() |
| 146 | + let lowestScore = Infinity |
| 147 | + let bestGrids: Set<string> = new Set() |
| 148 | + while (openTiles.size > 0) { |
| 149 | + openTiles.delete(current.hash) |
| 150 | + const networkNode = network.get(current.grid)! |
| 151 | + for (const [nextNodeHash, nextNodeInfo] of networkNode) { |
| 152 | + const [nextNodeGrid] = nextNodeHash.split('~') |
| 153 | + if (nextNodeInfo.startDir === flipDir(current.dir)) continue |
| 154 | + const cost = nextNodeInfo.cost |
| 155 | + const turnCost = nextNodeInfo.startDir === current.dir ? 0 : 1000 |
| 156 | + let score = current.score + cost + turnCost |
| 157 | + const nextNodeEndHash = `${nextNodeGrid}~${nextNodeInfo.endDir}` |
| 158 | + const bestHashScore = bestHashScores.get(nextNodeEndHash) |
| 159 | + const grids = new Set([...current.grids, ...nextNodeInfo.grids]) |
| 160 | + if (bestHashScore) { |
| 161 | + if (bestHashScore[0] < score) { |
| 162 | + continue |
| 163 | + } else if (bestHashScore[0] === score) { |
| 164 | + bestHashScore[1].forEach((g) => grids.add(g)) |
| 165 | + } |
| 166 | + } |
| 167 | + bestHashScores.set(nextNodeEndHash, [score, grids]) |
| 168 | + if (nextNodeGrid === endGrid) { |
| 169 | + if (score < lowestScore) { |
| 170 | + lowestScore = score |
| 171 | + bestGrids = new Set(grids) |
| 172 | + } else if (score === lowestScore) { |
| 173 | + grids.forEach((grid) => bestGrids.add(grid)) |
| 174 | + } |
| 175 | + break |
| 176 | + } |
| 177 | + const toNode: PathNode = { |
| 178 | + grid: nextNodeGrid, |
| 179 | + hash: nextNodeEndHash, |
| 180 | + dir: nextNodeInfo.endDir, |
| 181 | + score, |
| 182 | + grids: [...grids], |
| 183 | + } |
| 184 | + openTiles.set(toNode.hash, toNode) |
| 185 | + } |
| 186 | + current = getLowestScoreTile(openTiles) |
| 187 | + } |
| 188 | + return [lowestScore, bestGrids] as [number, Set<string>] |
| 189 | +} |
| 190 | + |
| 191 | +function getLowestScoreTile(list: Map<string, PathNode>) { |
| 192 | + let best |
| 193 | + for (const [, value] of list) { |
| 194 | + if (!best || value.score < best.score) { |
| 195 | + best = value |
| 196 | + } |
| 197 | + } |
| 198 | + return best! |
| 199 | +} |
| 200 | + |
| 201 | +export const getPart1Answer: Answer = (input, example = false) => { |
| 202 | + const { map, startXY, endXY } = parseInput(input) |
| 203 | + const [lowestScore] = findPath(map, startXY, endXY) |
| 204 | + return lowestScore |
| 205 | +} |
| 206 | + |
| 207 | +export const part1Examples: Example[] = [ |
| 208 | + [ |
| 209 | + `############### |
| 210 | +#.......#....E# |
| 211 | +#.#.###.#.###.# |
| 212 | +#.....#.#...#.# |
| 213 | +#.###.#####.#.# |
| 214 | +#.#.#.......#.# |
| 215 | +#.#.#####.###.# |
| 216 | +#...........#.# |
| 217 | +###.#.#####.#.# |
| 218 | +#...#.....#.#.# |
| 219 | +#.#.#.###.#.#.# |
| 220 | +#.....#...#.#.# |
| 221 | +#.###.#.#.#.#.# |
| 222 | +#S..#.....#...# |
| 223 | +###############`, |
| 224 | + '7036', |
| 225 | + ], |
| 226 | + [ |
| 227 | + `################# |
| 228 | +#...#...#...#..E# |
| 229 | +#.#.#.#.#.#.#.#.# |
| 230 | +#.#.#.#...#...#.# |
| 231 | +#.#.#.#.###.#.#.# |
| 232 | +#...#.#.#.....#.# |
| 233 | +#.#.#.#.#.#####.# |
| 234 | +#.#...#.#.#.....# |
| 235 | +#.#.#####.#.###.# |
| 236 | +#.#.#.......#...# |
| 237 | +#.#.###.#####.### |
| 238 | +#.#.#...#.....#.# |
| 239 | +#.#.#.#####.###.# |
| 240 | +#.#.#.........#.# |
| 241 | +#.#.#.#########.# |
| 242 | +#S#.............# |
| 243 | +#################`, |
| 244 | + '11048', |
| 245 | + ], |
| 246 | +] |
| 247 | + |
| 248 | +type NodeGraph = Map< |
| 249 | + string, |
| 250 | + Map<string, { startDir: number; cost: number; endDir: number; grids: string[] }> |
| 251 | +> |
| 252 | + |
| 253 | +export const getPart2Answer: Answer = (input, example = false) => { |
| 254 | + const { map, startXY, endXY } = parseInput(input) |
| 255 | + const [, bestPathGrids] = findPath(map, startXY, endXY) |
| 256 | + return bestPathGrids.size |
| 257 | +} |
| 258 | + |
| 259 | +export const part2Examples: Example[] = [ |
| 260 | + [part1Examples[0][0], '45'], |
| 261 | + [part1Examples[1][0], '64'], |
| 262 | +] |
0 commit comments