Robert Mion

Posted on

Parabolic Reflector Dish

Advent of Code 2023 Day 14

Part 1

Plenty of time to strategize

I first read the puzzle details several days ago.

I understood the challenge clearly.

I had a decent idea for what I want an algorithm to do.

But I wasn't sure how I'd get it to work.

I think I know well enough now to explore what's in my mind on the page with some code.

Start at the end: Sorting teeny-tiny substrings

That's what I intend to do.

`O`s first, then `.`s.

So, a substring like this:

``````.0...0.
``````

Should become this:

``````00.....
``````

The way sorting algorithms work is by comparing two items and either:

• Moving one before the other if the difference is `-1`
• Moving one after the other if the difference is `1`
• Moving nothing if the difference is `0`

Well, I'm not working with numbers.

So, I'll have to manually return `-1`, `1` and `0` based on whether the character is a `O` or `.`.

Something like this:

``````For each character in the substring
If the first character is a 0, return -1
Else if the first character is a . return 1
``````

There's no need to handle a case returning 0, I don't think.

Let's see if this does what I hope it will.

My algorithm in JavaScript:

``````str.split('').sort(
(a, b) => {
if (a == '0') return -1
if (a == '.') return 1
}
).join('')
``````

Running it on:

``````.0..0.0.
``````

Successfully generates:

``````000.....
``````

Woohoo!

Back to the beginning: padding the grid with `#`s

There's a vertical line in the example that, when rearranged in order to appear horizontal, looks like this:

``````.#.0.......
``````

As shown in the north-tilted output, that line must end up looking like this:

``````.#0........
``````

My algorithm above is prepared to take this string:

``````.0.......
``````

And generate:

``````0........
``````

But how would I isolate that substring from the larger string?

Well, Each `#` acts as a wall.

If I can identify the location of each `#` in a string, I can extract all characters between each `#` as a substring to sort.

But that will be tough with the example string above:

``````.#.0.......
``````

Sure, there's the one `#` in it. But it's not on an edge.

I'd need some pretty complicated logic to check whether the first and last `#`s are on edges.

And I'd rather not. I'd rather trust that there are or aren't.

So, I'll make it so there are.

Right from the start, I'll pad the grid with a border of `#`s.

So, what starts as:

``````.O#O
#.0.
.#.0
``````

Will become:

``````####
.O#O
#.0.
.#.0
####
``````

Now, I can be certain that `#`s bookend each string. And in most cases there will be at least one substring in each column of the grid to sort.

I suppose there could be a column filled with `#`s which would have no substrings. My algorithm should account for that naturally, too.

So, how can I pad the grid?

Well, first I have to make a grid from the puzzle input:

``````input.split('\n').map(line => line.split(''))
``````

That should take me from:

``````.O#O
#.0.
.#.0
``````

To:

``````[
['.','O','#','O'],
['#','.','O','.'],
['.','#','.','O'],
]
``````

Then, I need to insert two arrays. Both will be four elements. Each element will be a `#`:

``````grid.splice(0, 0, ['#','#','#','#'])
grid.splice(-1, 0, ['#','#','#','#'])
``````

Now the grid has `#`s for bookends...once I turn each column into a string.

Sanity check: writing and testing the code

I wrote my algorithm and tested on the example given.

I had to adjust my `splice()` calls for two reasons:

• Using `-1` inserts items just before the last item, not after. I used the length of the grid instead.
• The example has more than four columns - and my input has tens of columns - so I need a dynamic amount

Here's my working JavaScript algorithm for generating the padded grid:

``````let grid = input.split('\n').map(line => line.split(''))
grid.splice(
0,           0, new Array(grid[0].length).fill('#')
)
grid.splice(
grid.length, 0, new Array(grid[0].length).fill('#')
)
``````

Printing the joined grid string reveals:

``````##########
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
##########
``````

I can now safely attempt to extract substrings from each column string...once I identify the position of each `#`.

Finding each hashtag in a column

First, I need to turn each column into a string:

``````grid[0].map((char, index) => {
let colString = grid.map((row, i) => {
return grid[i][index]
}).join('')
})
``````

Viola! When printed, I see the grid rotated and flipped:

``````#OO.O.O..###
#...OO....O#
#.O...#O..O#
#.O.#......#
#.#.O......#
##.#..O#.###
#..#...O.#.#
#....O#.O#.#
#....#.....#
#.#.O.#O...#
``````

Next, I can use `regex` to match each `#` and generate the list of indices:

``````let hashtags = [...colString.matchAll(/#/g)].map(el => el.index)
``````

As expected, I get lists of numbers, each one containing a `0` and an `11` which represent the bookend `#`s, along with any others in the column string.

Now what?

Well, I need to slice up the substrings, sort, and concatenate together the final, larger, 'tilted' string!

Extracting each substring

I'll use this string as a working example:

``````#.O.#......#
``````

The `#` indices are:

``````[0, 4, 11]
``````

I therefore want the substrings from indices:

• `1-3`
• `5-10`

Hmm. So:

``````For each hashtag index except the last
Generate a substring from the location one greater than the current index up to and including the location one less than the number one greater than the current index
``````

That sounded real confusing, so here it is with numbers:

``````   [0, 4, 11]
i = 0  1  2

for i from 0 to 1
when i is 0
get substring from (0 + 1) to (4 - 1)

when i is 1
get substring from (4 + 1) to (11 - 1)
``````

That makes more sense, to me at least.

In some cases, this `for` loop may generate an empty string.

That should be ok, though.

I'm ultimately going to concatenate strings together. An empty string will leave a string practically unchanged.

My algorithm for extracting the substrings:

``````for (let h = 0; h < hashtags.length - 1; h++) {
let subString = colString.slice(
hashtags[h] + 1, hashtags[h + 1]
)
}
``````

Constructing the tilted string

I need hashtags in their original place and modified substrings between the right hashtags.

Here's my algorithm in JavaScript:

``````let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = colString.slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
``````

Analyzing my results:

``````Original #OO.O.O..###
Modified #OOOO....###
Original #...OO....O#
Modified #OOO.......#
Original #.O...#O..O#
Modified #O....#OO..#
Original #.O.#......#
Modified #O..#......#
Original #.#.O......#
Modified #.#O.......#
Original ##.#..O#.###
Modified ##.#O..#.###
Original #..#...O.#.#
Modified #..#O....#.#
Original #....O#.O#.#
Modified #O....#O.#.#
Original #....#.....#
Modified #....#.....#
Original #.#.O.#O...#
Modified #.#O..#O...#
``````

I like what I see!

Calculating the total load

Lastly, I need to sum up each round rock's value based on it's location in the tilted string.

I need to account for my padding of `#`s at each end, too.

So, in this modified string:

``````#OOOO....###
``````

The amounts by which to multiply each round rock are:

``````11 10 9 8 7 6 5 4 3 2 1 0
#  O O O O . . . . # # #
``````

Hmm. Perhaps I can make this easier by reversing the string first, then multiplying by the index.

``````0 1 2 3 4 5 6 7 8 9 10 11
# # # . . . . O O O O  #
``````

Ya, I like that better.

My algorithm in JavaScript:

``````let sum = finalString
.split('')
.reverse()
.reduce(
(accumulator, current, index) => {
if (current == 'O') {
accumulator += index
}
return accumulator
}, 0
)
``````

Works great!

Altogether now

Here's my full algorithm:

``````function tilt(a,b) {
if (a == 'O') return -1
if (a == '.') return 1
}
const part1 = grid[0].reduce((load, char, index) => {
let colString = grid.map((row, i) => {
return grid[i][index]
}).join('')
let hashtags = [...colString.matchAll(/#/g)].map(el => el.index)
let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = colString.slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
let sum = finalString
.split('')
.reverse()
.reduce(
(acc,curr,idx) => {
if (curr == 'O') {
acc += idx
}
return acc
}, 0
)
return load += sum
}, 0)
``````

By golly, it works, too!

It generates the correct answer for the example input.

And for my puzzle input!

Woohoo!!!

Part 2

It was inevitable, right?

• Checkpoint: a single direction, one time
• Real race: four directions, one million times

Because of course!

My problem won't be each direction.

It will be re-constructing the original grid layout after each tilt...since my algorithm reassembles columns and flips the grid horizontally and vertically.

Time to get back to work!

First: fixing north

As I just mentioned, my algorithm generates a mutated copy of the grid after 'tilting' it.

I now need to perform the tilt and ensure the resulting grid retains columns and rows, albeit 'sorted' correctly after a tilt.

Fast-forward: finished, after two obstacles

Obstacle #1: I needed to pad all four sides with `#`s first. I was hoping I could wait to do it, but I needed a square area grid.

Obstacle #2: I mistakenly attempted to access my original grid when I meant to access one of several intermediate grids. Ultimately, I just re-assign updated grids to the same variable to keep things foolproof.

Here's my completed `north`-tilting function:

``````function north() {
grid = grid[0].map((char, col) => {
let colString = grid.map((row, i) => {
return grid[i][col]
}).join('')
let hashtags = [...colString.matchAll(/#/g)]
.map(el => el.index)
let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = colString.slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
return finalString.split('')
})
grid = grid[0].map((char, col) => {
return grid.map((row, i) => {
return grid[i][col]
})
})
}
``````

Running it on the example grid generates these before-after states:

``````Before
############
#O....#....#
#O.OO#....##
#.....##...#
#OO.#O....O#
#.O.....O#.#
#O.#..O.#.##
#..O..#O..O#
#.......O..#
##....###..#
##OO..#....#
############

After
############
#OOOO.#.O..#
#OO..#....##
#OO..O##..O#
#O..#.OO...#
#........#.#
#..#....#.##
#..O..#.O.O#
#..O.......#
##....###..#
##....#....#
############
``````

Tilting south: one small change, twice

I really mean that. One small change. In two places.

I just have to reverse the strings I generate from each column. This simulates tilting in the other direction.

Here's my `south`-tilting function, with two added `reverse()` method calls:

``````function south() {
grid = grid[0].map((char, col) => {
let colString = grid.map((row, i) => {
return grid[i][col]
}).reverse().join('')
let hashtags = [...colString.matchAll(/#/g)]
.map(el => el.index)
let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = colString.slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
return finalString.split('').reverse()
})
grid = grid[0].map((char, col) => {
return grid.map((row, i) => {
return grid[i][col]
})
})
}
``````

Running it on the example grid generates these before-after states:

``````Before
############
#O....#....#
#O.OO#....##
#.....##...#
#OO.#O....O#
#.O.....O#.#
#O.#..O.#.##
#..O..#O..O#
#.......O..#
##....###..#
##OO..#....#
############

After
############
#.....#....#
#....#....##
#...O.##...#
#...#......#
#O.O....O#O#
#O.#..O.#.##
#O....#....#
#OO....OO..#
##OO..###..#
##OO.O#...O#
############
``````

Tilting west: the easiest of the four

Why the easiest?

• No generating strings from each column
• No need to reverse anything

Here's the function:

``````function west() {
grid = grid.map((row) => {
let hashtags = [...row.join('').matchAll(/#/g)]
.map(el => el.index)
let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = row.join('').slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
return finalString.split('')
})
}
``````

And the before-after:

``````Before
############
#O....#....#
#O.OO#....##
#.....##...#
#OO.#O....O#
#.O.....O#.#
#O.#..O.#.##
#..O..#O..O#
#.......O..#
##....###..#
##OO..#....#
############

After
############
#O....#....#
#OOO.#....##
#.....##...#
#OO.#OO....#
#OO......#.#
#O.#O...#.##
#O....#OO..#
#O.........#
##....###..#
##OO..#....#
############
``````

Tilting east: same small change, twice

Much in the same way `south` differed from `north`, I have to do two reversals: one initially and one again to reset things.

Here's the function:

``````function east() {
grid = grid.map((row) => {
row = row.reverse()
let hashtags = [...row.join('').matchAll(/#/g)]
.map(el => el.index)
let finalString = ""
for (let h = 0; h < hashtags.length - 1; h++) {
finalString += "#"
let subString = row.join('').slice(hashtags[h] + 1, hashtags[h + 1])
subString = subString.split('').sort(tilt).join('')
finalString += subString
}
finalString += "#"
return finalString.split('').reverse()
})
}
``````

And the before-after:

``````Before
############
#O....#....#
#O.OO#....##
#.....##...#
#OO.#O....O#
#.O.....O#.#
#O.#..O.#.##
#..O..#O..O#
#.......O..#
##....###..#
##OO..#....#
############

After
############
#....O#....#
#.OOO#....##
#.....##...#
#.OO#....OO#
#......OO#.#
#.O#...O#.##
#....O#..OO#
#.........O#
##....###..#
##..OO#....#
############
``````

Testing a few cycles on the example input

I get 3 cycles to use as tests for my algorithm.

Here's my algorithm that will run a complete cycle:

``````function cycle() {
north()
west()
south()
east()
}
``````

Running it once produces the expected grid!

Running it twice produces the expected grid!

Running it thrice produces the expected grid!

This is a good sign.

Now I need to re-incorporate my calculation of the total load.

Finishing with the total load calculation

Things get tricky because I padded my grid.

I have to account for the extra row on each end of the grid.

I need the numbers to the right as multipliers for each row:

``````############
#O....#....# 10
#O.OO#....## 9
#.....##...# 8
#OO.#O....O# 7
#.O.....O#.# 6
#O.#..O.#.## 5
#..O..#O..O# 4
#.......O..# 3
##....###..# 2
##OO..#....# 1
############
``````

To get that, I figure:

``````  Grid length = 12
- 2
------------------
10
- One less than index of current row
------------------
Correct multiplier amount
``````

For the row that should be 10

``````  12
- 2
-----
10
- (1 - 1) = 0
-----
10
``````

For the row that should be 9

``````  12
- 2
-----
10
- (2 - 1) = 1
-----
9
``````

And so on.

Here's my function with what I am fairly confident is correct math:

``````function calcLoad() {
return grid.reduce(
(sum, row, index) => {
return sum += [...row.join('').matchAll(/O/g)].length
* ( (grid.length - 2) - (index - 1) )
}, 0
)
}
``````

The part I overlooked: cycle completion speed

I have to simulate running a billion cycles!

I presumed that my algorithm would process cycles quickly - at least on the example input.

Sadly, these are the metrics:

• 80k cycles/min on example input
• <1k cycles/min on my puzzle input

So...there's no way I'll ever process a billion cycles with my multi-array-generating algorithm.

New strategy: identify whether grid state repeats

I'll bet that after enough cycles, the state of the grid repeats.

I'll further bet that I just have to figure that out and extrapolate it out far enough to find the grid's state after a billion cycles have completed.

Worth a try!

Collecting states and cycles completed

I think I need a dictionary that looks like this:

``````{
'O.#.O\n#..OO.\n': [3, 433, 866],
'#O..O\n##.O.O\n': [15, 448, 914],
}
``````

With that, I could:

• Print anytime a state has occurred
• Print all states and look for patterns in the difference between occurrences

Off to code!

...

I'm back.

This is my state-checking-and-saving function that gets called from my `for` loop:

``````function checkState(i) {
let state = grid.map(row => row.join('')).join('\n')
if (!(state in states)) {
states[state] = [i]
} else {
states[state].push(i)
}
}
``````

The results are in:

• The arrays are amassing values, as expected!
• The values in each array that tracks which cycle the repeated state occurred in differ by exactly 7!

This is great news.

Now, I have to find the state that repeats on a cycle interval that will eventually intersect with a billion.

Fast-forward: so much trial and error

Once I got the code above working, I just kept going.

And getting stuck.

Then going.

Then getting stuck.

How?

• I calculated the remainder after dividing 1B by 7. It's 6.
• I wrote code to find the state string corresponding to the array of repeated cycles with a 6 in it
• I updated the grid to reflect that state
• I calculated the total load
• I got an answer different from the expected answer

What was I doing wrong?

• My cycle loop starts at 0
• So, I should have looked for the remainder after dividing 999,999,999 by 7. It's 5.
• I ran the code to find the right state string
• I updated the grid
• I calculated the total load
• I got the correct answer!

I figured I could do the same for my puzzle input.

I was quite wrong.

How wrong was I?

• The example input's states start repeating almost immediately: after just a few cycles
• My puzzle input's states don't start repeating until after at least 100 cycles

It took a lot of troubleshooting to come to that realization.

Like, major scavenger hunt through multiple console logs.

But once I discovered that fact, I knew I was close to solving this part.

My cycles start repeating after the 102nd cycle.

They repeat every 14 cycles.

So, I needed to calculate:

• The nearest whole number after dividing 999,999,999 by 14
• The first number below that number in increments of 14 that, when increased by the correct number from 102-115, equates to 999,999,999

After doing some mental math, I determined that my magic winning number is 103:

`````` 999,999,999
/         14
------------
71,428,571
71,428,570
71,428,569
71,428,568
71,428,567
71,428,566
71,428,565
71,428,564 !!!
*         14
------------
999,999,896
+        103
------------
999,999,999
``````
• I ran the code to find the right state string whose array value started with 103
• I updated the grid
• I calculated the total load
• I got the correct answer!

Woohoo!!!

That took forever!

But, once again, I'm glad I persisted.

And now, for some time away...before moving on to Day 15.