DEV Community

Cover image for Parabolic Reflector Dish
Robert Mion
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.

Os first, then .s.

So, a substring like this:

.0...0.
Enter fullscreen mode Exit fullscreen mode

Should become this:

00.....
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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('')
Enter fullscreen mode Exit fullscreen mode

Running it on:

.0..0.0.
Enter fullscreen mode Exit fullscreen mode

Successfully generates:

000.....
Enter fullscreen mode Exit fullscreen mode

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.......
Enter fullscreen mode Exit fullscreen mode

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

.#0........
Enter fullscreen mode Exit fullscreen mode

My algorithm above is prepared to take this string:

.0.......
Enter fullscreen mode Exit fullscreen mode

And generate:

0........
Enter fullscreen mode Exit fullscreen mode

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.......
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Will become:

####
.O#O
#.0.
.#.0
####
Enter fullscreen mode Exit fullscreen mode

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(''))
Enter fullscreen mode Exit fullscreen mode

That should take me from:

.O#O
#.0.
.#.0
Enter fullscreen mode Exit fullscreen mode

To:

[
  ['.','O','#','O'],
  ['#','.','O','.'],
  ['.','#','.','O'],
]
Enter fullscreen mode Exit fullscreen mode

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, ['#','#','#','#'])
Enter fullscreen mode Exit fullscreen mode

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('#')
)
Enter fullscreen mode Exit fullscreen mode

Printing the joined grid string reveals:

##########
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
##########
Enter fullscreen mode Exit fullscreen mode

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('')
})
Enter fullscreen mode Exit fullscreen mode

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...#
Enter fullscreen mode Exit fullscreen mode

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

let hashtags = [...colString.matchAll(/#/g)].map(el => el.index)
Enter fullscreen mode Exit fullscreen mode

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.#......#
Enter fullscreen mode Exit fullscreen mode

The # indices are:

[0, 4, 11]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
  )
}
Enter fullscreen mode Exit fullscreen mode

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 += "#"
Enter fullscreen mode Exit fullscreen mode

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...#
Enter fullscreen mode Exit fullscreen mode

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....###
Enter fullscreen mode Exit fullscreen mode

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 . . . . # # #
Enter fullscreen mode Exit fullscreen mode

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  #
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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.......#
##....###..#
##....#....#
############
Enter fullscreen mode Exit fullscreen mode

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]
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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#
############
Enter fullscreen mode Exit fullscreen mode

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('')
  })
}
Enter fullscreen mode Exit fullscreen mode

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..#....#
############
Enter fullscreen mode Exit fullscreen mode

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()
  })
}
Enter fullscreen mode Exit fullscreen mode

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#....#
############
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
############
Enter fullscreen mode Exit fullscreen mode

To get that, I figure:

  Grid length = 12
- 2
------------------
  10
- One less than index of current row
------------------
  Correct multiplier amount
Enter fullscreen mode Exit fullscreen mode

For the row that should be 10

  12
- 2
-----
  10
- (1 - 1) = 0
-----
  10
Enter fullscreen mode Exit fullscreen mode

For the row that should be 9

  12
- 2
-----
  10
- (2 - 1) = 1
-----
  9
Enter fullscreen mode Exit fullscreen mode

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
  )
}
Enter fullscreen mode Exit fullscreen mode

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],
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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.

Top comments (0)