Solve 2024-12
Add side-aware coordinate utils
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="2024-12" type="BunRunConfiguration">
|
||||||
|
<option name="program" value="2024/12/run.ts" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { getSides, solveFirst, solveSecond } from './index.ts';
|
||||||
|
|
||||||
|
describe('2024-12', () => {
|
||||||
|
it('first', () => {
|
||||||
|
expect(
|
||||||
|
solveFirst(`AAAA
|
||||||
|
BBCD
|
||||||
|
BBCC
|
||||||
|
EEEC`)
|
||||||
|
).toBe(140);
|
||||||
|
expect(
|
||||||
|
solveFirst(`OOOOO
|
||||||
|
OXOXO
|
||||||
|
OOOOO
|
||||||
|
OXOXO
|
||||||
|
OOOOO`)
|
||||||
|
).toBe(772);
|
||||||
|
expect(
|
||||||
|
solveFirst(`RRRRIICCFF
|
||||||
|
RRRRIICCCF
|
||||||
|
VVRRRCCFFF
|
||||||
|
VVRCCCJFFF
|
||||||
|
VVVVCJJCFE
|
||||||
|
VVIVCCJJEE
|
||||||
|
VVIIICJJEE
|
||||||
|
MIIIIIJJEE
|
||||||
|
MIIISIJEEE
|
||||||
|
MMMISSJEEE`)
|
||||||
|
).toBe(1930);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('second', () => {
|
||||||
|
// it takes too long
|
||||||
|
// so only run while `NODE_ENV=production bun test --coverage`
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
expect(
|
||||||
|
solveSecond(`EEEEE
|
||||||
|
EXXXX
|
||||||
|
EEEEE
|
||||||
|
EXXXX
|
||||||
|
EEEEE`)
|
||||||
|
).toBe(236);
|
||||||
|
expect(
|
||||||
|
solveSecond(`AAAA
|
||||||
|
BBCD
|
||||||
|
BBCC
|
||||||
|
EEEC`)
|
||||||
|
).toBe(80);
|
||||||
|
expect(
|
||||||
|
solveSecond(`OOOOO
|
||||||
|
OXOXO
|
||||||
|
OOOOO
|
||||||
|
OXOXO
|
||||||
|
OOOOO`)
|
||||||
|
).toBe(436);
|
||||||
|
expect(
|
||||||
|
solveSecond(`AAAAAA
|
||||||
|
AAABBA
|
||||||
|
AAABBA
|
||||||
|
ABBAAA
|
||||||
|
ABBAAA
|
||||||
|
AAAAAA`)
|
||||||
|
).toBe(368);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sides', () => {
|
||||||
|
expect(
|
||||||
|
getSides([
|
||||||
|
[0, 0],
|
||||||
|
[1, 0],
|
||||||
|
[2, 0],
|
||||||
|
[0, 1]
|
||||||
|
])
|
||||||
|
).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { sumArray } from '../../utils/array.ts';
|
||||||
|
import {
|
||||||
|
calculateSides,
|
||||||
|
type Coordinates,
|
||||||
|
getAdjacent,
|
||||||
|
getNeighbouringSides,
|
||||||
|
isCoordinateEqual
|
||||||
|
} from '../../utils/coordinates.ts';
|
||||||
|
|
||||||
|
function searchNeighbours(
|
||||||
|
visited: Set<number>,
|
||||||
|
rows: string[],
|
||||||
|
char: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
found: Coordinates
|
||||||
|
) {
|
||||||
|
const position = y * rows.length + x;
|
||||||
|
if (visited.has(position)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(position);
|
||||||
|
found.push([x, y]);
|
||||||
|
|
||||||
|
const { left, top, right, down } = getAdjacent(rows, x, y);
|
||||||
|
if (left === char) {
|
||||||
|
searchNeighbours(visited, rows, char, x - 1, y, found);
|
||||||
|
}
|
||||||
|
if (top === char) {
|
||||||
|
searchNeighbours(visited, rows, char, x, y - 1, found);
|
||||||
|
}
|
||||||
|
if (right === char) {
|
||||||
|
searchNeighbours(visited, rows, char, x + 1, y, found);
|
||||||
|
}
|
||||||
|
if (down === char) {
|
||||||
|
searchNeighbours(visited, rows, char, x, y + 1, found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupChars(input: string) {
|
||||||
|
const rows = input.split('\n');
|
||||||
|
|
||||||
|
const groups: { char: string; coords: Coordinates }[] = [];
|
||||||
|
const visited = new Set<number>();
|
||||||
|
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
for (let x = 0; x < rows[y].length; x++) {
|
||||||
|
const position = y * rows.length + x;
|
||||||
|
if (visited.has(position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char = rows[y][x];
|
||||||
|
const coords: Coordinates = [];
|
||||||
|
searchNeighbours(visited, rows, char, x, y, coords);
|
||||||
|
|
||||||
|
groups.push({ char, coords });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPerimeter(g: Coordinates): number {
|
||||||
|
if (g.length === 1) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (g.length === 2) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
const { uniqueSides } = calculateSides(g);
|
||||||
|
|
||||||
|
return uniqueSides.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function solveFirst(input: string): number {
|
||||||
|
const groups = groupChars(input);
|
||||||
|
|
||||||
|
return sumArray(groups.map((g) => g.coords.length * getPerimeter(g.coords)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function solveSecond(input: string): number {
|
||||||
|
const groups = groupChars(input);
|
||||||
|
|
||||||
|
return sumArray(groups.map((g) => g.coords.length * getSides(g.coords)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSides(g: Coordinates): number {
|
||||||
|
if (g.length === 1) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (g.length === 2) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const { uniqueSides } = calculateSides(g);
|
||||||
|
|
||||||
|
const visited = new Set<number>();
|
||||||
|
|
||||||
|
let sides = 0;
|
||||||
|
for (let i = 0; i < uniqueSides.length; i++) {
|
||||||
|
if (visited.has(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(i);
|
||||||
|
sides++;
|
||||||
|
|
||||||
|
const { neighbours } = getNeighbouringSides(uniqueSides[i], uniqueSides);
|
||||||
|
|
||||||
|
neighbours.forEach((c) =>
|
||||||
|
visited.add(uniqueSides.findIndex((v) => isCoordinateEqual(v, c)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sides;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { solveFirst, solveSecond } from './index.ts';
|
||||||
|
|
||||||
|
const input = await Bun.file(join(__dirname, 'input.txt')).text();
|
||||||
|
|
||||||
|
const firstAnswer = solveFirst(input);
|
||||||
|
|
||||||
|
console.log(firstAnswer);
|
||||||
|
|
||||||
|
const secondAnswer = solveSecond(input);
|
||||||
|
|
||||||
|
console.log(secondAnswer);
|
||||||
+56
-3
@@ -1,5 +1,16 @@
|
|||||||
|
export const Sides = {
|
||||||
|
TOP: 0,
|
||||||
|
RIGHT: 1,
|
||||||
|
DOWN: 2,
|
||||||
|
LEFT: 3
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Side = (typeof Sides)[keyof typeof Sides];
|
||||||
|
|
||||||
export type Coordinate = [number, number];
|
export type Coordinate = [number, number];
|
||||||
|
export type CoordinateSide = [number, number, Side];
|
||||||
export type Coordinates = Coordinate[];
|
export type Coordinates = Coordinate[];
|
||||||
|
export type CoordinateSides = CoordinateSide[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a filter lambda to be used in array.filter() on a xy-Coordinate array (array of two number arrays).
|
* Returns a filter lambda to be used in array.filter() on a xy-Coordinate array (array of two number arrays).
|
||||||
@@ -9,7 +20,7 @@ export type Coordinates = Coordinate[];
|
|||||||
* @param yMin Inclusive, defaults to 0
|
* @param yMin Inclusive, defaults to 0
|
||||||
*/
|
*/
|
||||||
export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) {
|
export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) {
|
||||||
return ([x, y]: Readonly<Coordinate>) =>
|
return ([x, y]: Readonly<Coordinate | CoordinateSide>) =>
|
||||||
x >= xMin && x < xMax && y >= yMin && y < yMax;
|
x >= xMin && x < xMax && y >= yMin && y < yMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +30,8 @@ export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) {
|
|||||||
* @param b xy-Coordinate, two number array
|
* @param b xy-Coordinate, two number array
|
||||||
*/
|
*/
|
||||||
export function isCoordinateEqual(
|
export function isCoordinateEqual(
|
||||||
a: Readonly<Coordinate>,
|
a: Readonly<Coordinate | CoordinateSide>,
|
||||||
b: Readonly<Coordinate>
|
b: Readonly<Coordinate | CoordinateSide>
|
||||||
): boolean {
|
): boolean {
|
||||||
return b[0] === a[0] && b[1] === a[1];
|
return b[0] === a[0] && b[1] === a[1];
|
||||||
}
|
}
|
||||||
@@ -78,3 +89,45 @@ export function findCoordsOfDigit(
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateSides(g: Coordinate[]) {
|
||||||
|
const allSides = g.flatMap(
|
||||||
|
([x, y]) =>
|
||||||
|
[
|
||||||
|
[x - 0.5, y, Sides.LEFT],
|
||||||
|
[x, y - 0.5, Sides.TOP],
|
||||||
|
[x + 0.5, y, Sides.RIGHT],
|
||||||
|
[x, y + 0.5, Sides.DOWN]
|
||||||
|
] as CoordinateSides
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueSides = allSides.filter(
|
||||||
|
(c) => allSides.filter((b) => isCoordinateEqual(c, b)).length === 1
|
||||||
|
);
|
||||||
|
return { uniqueSides, allSides };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNeighbouringSides(
|
||||||
|
from: CoordinateSide,
|
||||||
|
all: CoordinateSides
|
||||||
|
) {
|
||||||
|
const [x, y, side] = from;
|
||||||
|
const isVertical = Math.abs(x % 1) === 0.5;
|
||||||
|
const coordIndex = isVertical ? 1 : 0;
|
||||||
|
|
||||||
|
const allDirectional = all
|
||||||
|
.filter(([cx, cy, cs]) => (isVertical ? cx === x : cy === y) && cs === side)
|
||||||
|
.toSorted((a, b) => a[coordIndex] - b[coordIndex]);
|
||||||
|
|
||||||
|
const before = allDirectional
|
||||||
|
.filter((d) => d[coordIndex] - from[coordIndex] < 0)
|
||||||
|
.toSorted((a, b) => b[coordIndex] - a[coordIndex])
|
||||||
|
.filter((d, i) => d[coordIndex] - from[coordIndex] === 0 - i - 1);
|
||||||
|
const after = allDirectional
|
||||||
|
.filter((d) => d[coordIndex] - from[coordIndex] > 0)
|
||||||
|
.filter((d, i) => d[coordIndex] - from[coordIndex] === i + 1);
|
||||||
|
|
||||||
|
const neighbours = [...before, ...after];
|
||||||
|
|
||||||
|
return { before, after, neighbours };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user