diff --git a/days/12/main.ts b/days/12/main.ts new file mode 100644 index 0000000..472a072 --- /dev/null +++ b/days/12/main.ts @@ -0,0 +1,160 @@ +import { Grid } from "@pal/datastructures"; +import { loadInput } from "utils"; + +export function part1(input: Array) { + let total = 0; + for (const region of extractRegions(input)) { + total += region.size * calculatePerimeter(region); + } + return total; +} + +export function part2(input: Array) { + let total = 0; + for (const region of extractRegions(input)) { + total += region.size * calculateNumberOfSides(region); + } + return total; +} + +type Coord = { x: number; y: number }; +type CoordId = string; +type Region = Set; + +function* extractRegions(input: Array): Generator { + const grid = Grid.fromStrings(input); + const regionsByType = new Map>(); + function addToRegion( + x: number, + y: number, + dx: number, + dy: number, + type: string, + regionsOfType: Set, + ): Region | null { + const neighbourType = grid.at(x + dx, y + dy); + if (neighbourType === type) { + for (const region of regionsOfType) { + if (region.has(coordId({ x: x + dx, y: y + dy }))) { + region.add(coordId({ x, y })); + return region; + } + } + } + return null; + } + for (let y = 0; y < grid.height; y++) { + for (let x = 0; x < grid.width; x++) { + const type = grid.at(x, y)!; + let regionsOfType = regionsByType.get(type); + if (regionsOfType == null) { + regionsOfType = new Set(); + regionsByType.set(type, regionsOfType); + } + const leftRegion = addToRegion(x, y, -1, 0, type, regionsOfType); + const rightRegion = addToRegion(x, y, 0, -1, type, regionsOfType); + if ( + leftRegion != null && rightRegion != null && leftRegion !== rightRegion + ) { + for (const coord of rightRegion) { + leftRegion.add(coord); + } + regionsOfType.delete(rightRegion); + } + if (leftRegion == null && rightRegion == null) { + regionsOfType.add(new Set([coordId({ x, y })])); + } + } + } + for (const regions of regionsByType.values()) { + yield* regions; + } +} + +function calculatePerimeter(region: Region): number { + let perimeter = 0; + for (const c of region) { + const { x, y } = fromCoordId(c); + if (region.has(coordId({ x: x - 1, y })) === false) { + perimeter++; + } + if (region.has(coordId({ x: x + 1, y })) === false) { + perimeter++; + } + if (region.has(coordId({ x, y: y - 1 })) === false) { + perimeter++; + } + if (region.has(coordId({ x, y: y + 1 })) === false) { + perimeter++; + } + } + return perimeter; +} + +function calculateNumberOfSides(region: Region): number { + const verticalLeftSides = new Map>(); + const verticalRightSides = new Map>(); + const horizontalTopSides = new Map>(); + const horizontalBottomSides = new Map>(); + for (const c of region) { + const { x, y } = fromCoordId(c); + if (region.has(coordId({ x: x - 1, y })) === false) { + computeIfAbsent(verticalLeftSides, x, () => []).push(y); + } + if (region.has(coordId({ x: x + 1, y })) === false) { + computeIfAbsent(verticalRightSides, x, () => []).push(y); + } + if (region.has(coordId({ x, y: y - 1 })) === false) { + computeIfAbsent(horizontalTopSides, y, () => []).push(x); + } + if (region.has(coordId({ x, y: y + 1 })) === false) { + computeIfAbsent(horizontalBottomSides, y, () => []).push(x); + } + } + return [ + ...verticalLeftSides.values(), + ...verticalRightSides.values(), + ...horizontalTopSides.values(), + ...horizontalBottomSides.values(), + ].map(getNumberOfContinousBlocks).reduce((a, b) => a + b, 0); +} + +function coordId(coord: Coord): CoordId { + return `${coord.x},${coord.y}`; +} + +function fromCoordId(coordId: CoordId): Coord { + const [x, y] = coordId.split(",").map(Number); + return { x, y }; +} + +function computeIfAbsent( + map: Map, + key: K, + defaultValue: () => V, +): V { + let value = map.get(key); + if (value == null) { + value = defaultValue(); + map.set(key, value); + } + return value; +} + +function getNumberOfContinousBlocks(numbers: Array) { + const sorted = numbers.toSorted(); + let count = 1; + let last = sorted[0]; + for (let i = 1; i < sorted.length; i++) { + if (sorted[i] !== last + 1) { + count++; + } + last = sorted[i]; + } + return count; +} + +if (import.meta.main) { + console.log(part1(await loadInput(12))); + console.log(part2(await loadInput(12))); +} diff --git a/days/12/main_test.ts b/days/12/main_test.ts new file mode 100644 index 0000000..d13f334 --- /dev/null +++ b/days/12/main_test.ts @@ -0,0 +1,67 @@ +import { expect } from "@std/expect"; +import { describe, it } from "@std/testing/bdd"; +import { part1, part2 } from "./main.ts"; +import { loadInput } from "utils"; + +describe("day 12 example", () => { + const input = [ + "RRRRIICCFF", + "RRRRIICCCF", + "VVRRRCCFFF", + "VVRCCCJFFF", + "VVVVCJJCFE", + "VVIVCCJJEE", + "VVIIICJJEE", + "MIIIIIJJEE", + "MIIISIJEEE", + "MMMISSJEEE", + ]; + + it("part 1", () => { + expect(part1(input)).toBe(1930); + }); + + it("part 2", () => { + expect(part2(input)).toBe(1206); + }); + + it("part 2 small example 1", () => { + expect(part2([ + "AAAAAA", + "AAABBA", + "AAABBA", + "ABBAAA", + "ABBAAA", + "AAAAAA", + ])).toBe(368); + }); + + it("part 2 small example 2", () => { + expect(part2([ + "EEEEE", + "EXXXX", + "EEEEE", + "EXXXX", + "EEEEE", + ])).toBe(236); + }); + + it("part 2 small example 3", () => { + expect(part2([ + "AAAA", + "BBCD", + "BBCC", + "EEEC", + ])).toBe(80); + }); +}); + +describe("day 12 solution", () => { + it("part 1", async () => { + expect(part1(await loadInput(12))).toBe(1467094); + }); + + it("part 2", async () => { + expect(part2(await loadInput(12))).toBe(881182); + }); +}); diff --git a/days/mod.ts b/days/mod.ts index 620d2e1..b531c80 100644 --- a/days/mod.ts +++ b/days/mod.ts @@ -3,7 +3,7 @@ type Solution = { part1: PartFunction; part2: PartFunction }; const days = new Map(); -for (const day of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) { +for (const day of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) { days.set(day, await import(`./${day}/main.ts`)); } diff --git a/inputs b/inputs index 94fe764..d325ce4 160000 --- a/inputs +++ b/inputs @@ -1 +1 @@ -Subproject commit 94fe7640ff0c710170a620f5596acd3738cf303d +Subproject commit d325ce4a9f73afd0b033d07cc1f2fde2ccef52e9