Skip to content

Commit 8ec8e80

Browse files
feat(2021-day-09): find the largest basins
Solves part 2 I was naughty and didn't follow TDD for this one. Mistakenly thought the logic would be more terse than it ended up being.
1 parent c445f40 commit 8ec8e80

File tree

2 files changed

+154
-26
lines changed

2 files changed

+154
-26
lines changed

2021/day-09/basins.js

Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
1+
const isLow = (val, col, row, rows) => {
2+
// Collect points in each cardinal direction
3+
const points = []
4+
// TODO: If supporting diagonal checks, use this logic instead to loop
5+
// for (let x = -1; x <= 1; x++) {
6+
// for (let y = -1; y <= 1; y++) {
7+
// if(x != 0 && y != 0)
8+
// if(rows[row + y] && rows[row + y][col + x]
9+
// }
10+
// }
11+
if (rows[row - 1] && rows[row - 1][col]) { points.push(parseInt(rows[row - 1][col])) }
12+
if (rows[row + 1] && rows[row + 1][col]) { points.push(parseInt(rows[row + 1][col])) }
13+
if (rows[row] && rows[row][col - 1]) { points.push(parseInt(rows[row][col - 1])) }
14+
if (rows[row] && rows[row][col + 1]) { points.push(parseInt(rows[row][col + 1])) }
15+
16+
// NOTE - if the value is the same as a neighbor,
17+
// that isn't counted as a low (even though together, they can be a low)
18+
// ... this might be a concern for part 2 ....
19+
return (val < Math.min(...points)) // value should be lower than any other points
20+
}
21+
122
const findLocalLows = (data) => {
223
const lows = []
324
const rows = data.split('\n')
425
let checked = 0
526

6-
const isLow = (val, col, row) => {
7-
// Collect points in each cardinal direction
8-
const points = []
9-
// TODO: If supporting diagonal checks, use this logic instead to loop
10-
// for (let x = -1; x <= 1; x++) {
11-
// for (let y = -1; y <= 1; y++) {
12-
// if(x != 0 && y != 0)
13-
// if(rows[row + y] && rows[row + y][col + x]
14-
// }
15-
// }
16-
if (rows[row - 1] && rows[row - 1][col]) { points.push(parseInt(rows[row - 1][col])) }
17-
if (rows[row + 1] && rows[row + 1][col]) { points.push(parseInt(rows[row + 1][col])) }
18-
if (rows[row] && rows[row][col - 1]) { points.push(parseInt(rows[row][col - 1])) }
19-
if (rows[row] && rows[row][col + 1]) { points.push(parseInt(rows[row][col + 1])) }
20-
21-
// NOTE - if the value is the same as a neighbor,
22-
// that isn't counted as a low (even though together, they can be a low)
23-
// ... this might be a concern for part 2 ....
24-
return (val < Math.min(...points)) // value should be lower than any other points
25-
}
26-
2727
rows.forEach((row, rowIdx) => {
2828
for (let c = 0; c < row.length; c++) {
2929
const cell = parseInt(row[c])
30-
if (isLow(cell, c, rowIdx)) {
30+
if (isLow(cell, c, rowIdx, rows)) {
3131
lows.push(cell)
3232
console.debug(`Found low at ${c},${rowIdx}: ${cell}`)
3333
}
@@ -39,6 +39,118 @@ const findLocalLows = (data) => {
3939
return lows
4040
}
4141

42+
const flow = (col, row, map, data, source) => {
43+
// Don't test invalid points
44+
if (col < 0 || col >= map.coords[0].length) {
45+
console.debug(`${col},${row} is out of bounds`)
46+
// Exceeds map horizontally
47+
return {
48+
map,
49+
result: false
50+
}
51+
}
52+
if (row < 0 || row >= map.coords.length) {
53+
console.debug(`${col},${row} is out of bounds`)
54+
// Exceeds map vertically
55+
return {
56+
map,
57+
result: false
58+
}
59+
}
60+
61+
// If the point is a peak, register and don't continue
62+
if (parseInt(data[row][col]) === 9) {
63+
console.debug(`${col},${row} is a peak.`)
64+
// Peaks aren't part of basins
65+
map.coords[row][col] = 'p'
66+
return {
67+
map,
68+
result: false
69+
}
70+
}
71+
72+
// If the point is higher than the source, we can't drain
73+
// BIG ASSUMPTION here about equal-height points
74+
if (data[row][col] >= source) {
75+
console.debug(`${col},${row} is higher (${data[row][col]} >= ${source}). Water can't flow uphill.`)
76+
return {
77+
map,
78+
result: false
79+
}
80+
}
81+
82+
// If the point already mapped to a basin, don't recalculate its flow
83+
if (map.coords[row] && map.coords[row][col]) {
84+
console.debug(`${col},${row} is already known to be in basin ${map.coords[row][col]}`)
85+
return {
86+
map,
87+
result: map.coords[row][col]
88+
}
89+
}
90+
91+
// If we've reached a low point, stop tracing
92+
if (isLow(data[row][col], col, row, data)) {
93+
console.debug(`${col},${row} is a low point in basin.`)
94+
// register a basin with an area of 1
95+
map.basins.push(1)
96+
// mark the low point to the basin
97+
map.coords[row][col] = map.basins.length - 1
98+
console.debug(`registered basin ${map.basins.length - 1}`)
99+
return {
100+
map,
101+
result: map.coords[row][col]
102+
}
103+
// HUGE ASSUMPTION that each basin only has 1 low point
104+
}
105+
106+
console.debug(`checking where point ${col},${row} drains to`)
107+
108+
// Check the next points in each cardinal direction
109+
const drains = []
110+
let result = false
111+
result = flow(col + 1, row, map, data, data[row][col]) // right
112+
map = result.map
113+
drains.push(result.result)
114+
result = flow(col - 1, row, map, data, data[row][col]) // left
115+
map = result.map
116+
drains.push(result.result)
117+
result = flow(col, row - 1, map, data, data[row][col]) // up
118+
map = result.map
119+
drains.push(result.result)
120+
result = flow(col, row + 1, map, data, data[row][col]) // down
121+
map = result.map
122+
drains.push(result.result)
123+
124+
const results = drains.filter((c) => c !== false)
125+
if (results.length > 1) {
126+
console.warn('Point has more than one possilbe drain.')
127+
const uniqueDrains = [...new Set(results)]
128+
if (uniqueDrains.length > 1) {
129+
console.debug(drains)
130+
throw new Error('Point drains into multiple drains. Data might be bad.')
131+
}
132+
// Otherwise, all drains go to the same basin, so that's the same as having 1 drain
133+
}
134+
if (results.length === 0) {
135+
console.debug(drains)
136+
throw new Error('Point is not the low, but has no drains. Data might be bad.')
137+
}
138+
139+
const basin = parseInt(results[0])
140+
141+
// Mark the point as belonging to the basin it drains into
142+
map.coords[row][col] = basin
143+
// Track the area of the basin so we don't have to recalculate it later
144+
map.basins[basin]++
145+
146+
// return the findings recursively
147+
return {
148+
map,
149+
result: map.coords[row][col]
150+
}
151+
}
152+
42153
module.exports = {
43-
findLocalLows
154+
findLocalLows,
155+
flow
44156
}

2021/day-09/solution.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const fs = require('fs')
22
const path = require('path')
33
const filePath = path.join(__dirname, 'input.txt')
44
const { linesToArray } = require('../../2018/inputParser')
5-
const { findLocalLows } = require('./basins')
5+
const { findLocalLows, flow } = require('./basins')
66

77
fs.readFile(filePath, { encoding: 'utf8' }, (err, initData) => {
88
if (err) throw err
@@ -24,8 +24,24 @@ fs.readFile(filePath, { encoding: 'utf8' }, (err, initData) => {
2424

2525
const part2 = () => {
2626
const data = resetInput()
27-
// console.debug(data)
28-
return 'No answer yet'
27+
let map = {
28+
coords: [...Array(data.length)].map(() => Array(data[0].length)), // allocate a map
29+
basins: [0] // fake first basin with no area to avoid having to figure out JS loose-type false/truthy and <> bullshit
30+
}
31+
32+
for (let x = 0; x < data[0].length; x++) {
33+
for (let y = 0; y < data.length; y++) {
34+
console.debug(`Starting flow trace at ${x},${y}`)
35+
map = flow(x, y, map, data, 9).map // 9 is a magic number for assuming we start with fresh drains
36+
}
37+
}
38+
39+
console.debug(`Found ${map.basins.length} basins.`)
40+
41+
// Multiply the area of the 3 largest basins
42+
return map.basins.sort((a, b) => a - b)
43+
.slice(map.basins.length - 3)
44+
.reduce((a, b) => a * b)
2945
}
3046
const answers = []
3147
answers.push(part1())

0 commit comments

Comments
 (0)