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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy