DEV Community

Robert Mion
Robert Mion

Posted on

Pulse Propagation

Advent of Code 2023 Day 20

Part 1

Highs and lows

Nothing exciting here. Just:

  • A dictionary
  • A queue
  • Inputs and outputs
  • Tracked state
  • Conditional forwarding
  • String manipulation

It probably won't be as fun as some other challenges.

But it should definitely prove...challenging.

Let's do this!

Parsing the input into a dictionary of modules

Given this input list of modules:

broadcaster -> a, b, c
%a -> b
%b -> c
%c -> inv
&inv -> a
Enter fullscreen mode Exit fullscreen mode

I think I want to build a dictionary that resembles this architecture:

{
  broadcaster: {
    destinations: ['a','b','c'],
    pulse: false,
    type: null
  },
  a: {
    destinations: ['b'],
    power: false,
    type: 'flip-flop',
  },
  b: {
    destinations: ['c'],
    power: false,
    type: 'flip-flop'
  },
  c: {
    destinations: ['inv'],
    power: false,
    type: 'flip-flop'
  },
  inv: {
    destinations: ['a'],
    pulses: [false],
    type: 'conjunction'
  },
}
Enter fullscreen mode Exit fullscreen mode

Writing that out made me have to re-read the rules several times.

I still don't quite understand all there is to know about pulses, remembering old pulses, and how some modules turn on and off.

But I'm sure I'll figure it out as I'm writing all the conditional logic to hopefully solve this puzzle!

Anyhoo, back to building this dictionary.

Enter: split().

First, on the ->:

broadcaster -> a, b, c
Enter fullscreen mode Exit fullscreen mode

becomes:

['broadcaster', 'a, b, c']
Enter fullscreen mode Exit fullscreen mode

Next, on the second element, only if there is a comma:

str.includes(',') ? str.split(', ') : str
Enter fullscreen mode Exit fullscreen mode

becomes:

['broadcaster', ['a','b','c']]
Enter fullscreen mode Exit fullscreen mode

And what about handling that first character?

Well, since every module except broadcaster has it, I can write a condition that, if passed, slices off and stores the first character:

let moduleType = name.slice(0,1)
name = name.slice(1)
Enter fullscreen mode Exit fullscreen mode

It's time to actually write this code and programmatically craft this dictionary.

...

I coded it. It's not pretty. And I'll probably have to change parts of it as I familiarize myself with the module rules.

But my code generates a dictionary of modules with destinations, initial pulse values, and types.

Push the button!...and troubleshoot

Let's dive in by simulating a button press and debugging my way toward the expected result.

A button press is eventually going to be a single iteration of a 1000-iteration loop.

So, I'll use a for loop that runs once:

for (let i = 0; i < 1; i++) {

}
Enter fullscreen mode Exit fullscreen mode

Somehow I need to make sure the program:

  • Starts with broadcaster
  • Touches every module that ever appears in a destination list
  • Processes all modules in the current modules' destination list before proceeding to the next module

I'm envisioning a queue data structure and a while loop that runs as long as the queue isn't empty.

let todo = ['broadcaster']
while (todo.length > 0) {

}
Enter fullscreen mode Exit fullscreen mode

For now, I'll track the pulse being sent:

let pulse = false
Enter fullscreen mode Exit fullscreen mode

Processing the queue and adding items to it:

while (todo.length > 0) {
  let next = todo.shift()
  modules[next].destinations.forEach(d => {
    todo.push(d)
    // more important things
  })
}
Enter fullscreen mode Exit fullscreen mode
First bug: not the right data type

My program kept generating an error, saying destinations doesn't exist.

Why was this?

Because sometimes I push a string to todo, and other times a push a 2-element array.

With a string, modules[next] finds a key.

With an array, modules[next] finds undefined.

One solution is to match the array data type when I craft the broadcaster key's value.

It's not great, but it gets rid of the error.

Second bug: endless loop

I assumed that queue of modules would eventually disipate.

I was wrong.

It mostly just grows.

Because every visited module has at least one destination. And each destination is being added to the list!

So, what am I missing?

Well, for staters, there's this important detail:

If a flip-flop module receives a high pulse, it is ignored and nothing happens.

That explains why the first simulated button press of the first example ends with:

inv -high-> a
Enter fullscreen mode Exit fullscreen mode

At least I now have a verifiable loop termination condition.

The question is, how will I trigger it?

Nevermind that.

The above condition shouldn't even end a loop.

It just means that the receiving module triggers no further pulses.

The real terminating rule is:

After pushing the button, you must wait until all pulses have been delivered and fully handled before pushing it again. Never push the button if modules are still processing pulses.

"...wait until all pulses have been delivered and fully handled...".

Rethinking how I track pulses

Pressing the button triggers a single low pulse with broadcaster as its destination.

So, my list of pulses is:

[
  [false, 'broadcaster']
]
Enter fullscreen mode Exit fullscreen mode

I pull it from the list and apply it to broadcaster.

At this moment, the list is empty.

broadcaster has three destinations, and passes its received pulse to each of them.

So, my list of pulses after broadcaster is done looks like:

[
  [false, 'a'],
  [false, 'b'],
  [false, 'c']
]
Enter fullscreen mode Exit fullscreen mode

I pull the first pulse from the list and apply it to a.

  • a is a flip-flop module
  • It starts as off
  • It gets a low pulse
  • It flips to on
  • It generates a high pulse to each destination
  • It only have one, b

After processing a, my pulse list looks like:

[
  [false, 'b'],
  [false, 'c'],
  [true, 'b']
]
Enter fullscreen mode Exit fullscreen mode

The next two pulses act similar to the previous one.

Thus, the pulse list changes like this:

[
  [false, 'c'],
  [true, 'b'],
  [true, 'c']
]
Enter fullscreen mode Exit fullscreen mode

And then:

[
  [true, 'b'],
  [true, 'c'],
  [true, 'inv']
]
Enter fullscreen mode Exit fullscreen mode

Now b receives a high pulse.

As stated in the rules, nothing happens.

No new pulses are added to the list.

[
  [true, 'c'],
  [true, 'inv']
]
Enter fullscreen mode Exit fullscreen mode

Now c receives a high pulse.

As stated in the rules, nothing happens.

No new pulses are added to the list.

[
  [true, 'inv']
]
Enter fullscreen mode Exit fullscreen mode

Now inv receives a high pulse.

  • It's a conjunction module
  • It defaults to remembering a low pulse from each input module
  • It updates its memory for the inputting module, c

Looks like I need to also track the originating module in my pulses list?

Anyway, since inv remembers a low pulse for c, it sends a high pulse to its destinations: a.

The pulse list - with sending module - is now:

[
  ['inv', true, 'a']
]
Enter fullscreen mode Exit fullscreen mode

Now a receives a high pulse.

It does nothing.

No new pulses are generated.

And the pulse list is empty.

So this round is complete.

I think I finally understand the first example!

Better late than never!

Now how the heck am I going to re-create this programmatically??!!

Back to the code...and the endless struggle

Here's the current state of my one-iteration for loop with soon-to-be-terminating while loop:

for (let i = 0; i < 1; i++) {
  let todo = [['button', false, 'broadcaster']]
  let pulse = false
  while (todo.length > 0) {
    let [origin, bool, dest] = todo.shift()    
    modules[origin].destinations.forEach(d => {
      switch (modules[origin].type) {
        case 'broadcaster':
          todo.push([
            'broadcaster',
            pulse,
            d
          ])
          break;
        case 'flip-flop':
          if (pulse == false) {
            if (modules[origin].power) {
              todo.push([
                origin,
                false,
                dest
              ])
              modules[origin].power = false
            } else {
              todo.push([
                origin,
                true,
                dest
              ])
              modules[origin].power = true
            }
          }
          break;
        case 'conjunction':
          break;
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

I believe I have accounted for two of the three module types.

But that third one has thrown a wrench at me.

I overlooked how complicated this detail would be:

Conjunction modules (prefix &) remember the type of the most recent pulse received from each of their connected input modules

My module-dictionary-maker only saves a module's outputs.

Not their inputs.

I need some way to store each input for each conjunction module.

I have an inkling of a strategy.

Generating all inputs to all conjunction modules

I'll add a inputs property to my conjunction modules that starts as an empty list.

I think I can do this after creating my dictionary.

Create a list of conjunction modules from the filtered dictionary keys
For each module, iterate through the dictionary's keys and values
If a key has that module as a destination
Add that key to the module's list of inputs
Enter fullscreen mode Exit fullscreen mode

Time to give it a try!

...

Thankfully that wasn't too tough.

And doing it helped me clean up my dictionary generation code.

Here's the input-cataloguing code:

let conjunctions = []
for (let key in modules) {
  if (modules[key].type == 'conjunction') {
    conjunctions.push(key)
  }
}
conjunctions.forEach(c => {
  for (let key in modules) {
    if (modules[key].destinations.includes(c)) {
      modules[c].inputs[key] = false
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

After running on both example inputs, it seems to be making the correct architecture for both dictionaries.

And now....back to handling conjunction-type modules!

Tracking the pulses of input modules

I wrote some code that seemed to match the spec as described.

For fun, I ran one iteration.

Immediate error.

I had to re-visit my first item in todo:

['button', false, 'broadcaster']
Enter fullscreen mode Exit fullscreen mode

My modules dictionary has no key for button, nor should it!

I need to manually perform the first step of sending a low signal to each of broadcaster's destination modules.

It's redundant and icky.

But it does the job.

I ran the program again for a single iteration.

It finished without errors!

I saw the same log of actions as the first example describes!

How about example 2?

I ran for a single iteration.

Another error.

This has to do with output not being a key in my dictionary.

I updated my code, wrapping almost all of the logic in a condition that checks for a key's existence in the dictionary.

I ran for a single iteration again.

It finished without errors!

I saw what seemed like the same log of steps as described after a single button push.

I ran for two iterations.

Again, it all seemed to match up.

I ran for three iterations.

Uh-oh. Mismatch detected.

A conjunction module was sending the wrong pulse to its destination module.

I'm not sure why.

After some extensive logging, I noticed that my program gets things wrong after the first iteration of the second example input.

I think I'm misunderstanding the first rule about conjunction modules:

Conjunction modules (prefix &) remember the type of the most recent pulse received from each of their connected input modules; they initially default to remembering a low pulse for each input. When a pulse is received, the conjunction module first updates its memory for that input.

I took that to mean when a pulse is received by the input module.

But I think it means when a pulse is received by the conjunction module.

They remember the type of the most recent pulse received from each of their connected input modules.

So when a conjunction module is a destination module, it must update the pulse type in its input list for the originating module to the pulse type being passed to it.

I think...???

Honestly, I'm so confused at this point.

...

I moved and duplicated the code that updated the boolean value for any key inside a conjunction module's input object.

It now has the potential to happen any time a new todo is added to the list, since that is when a pulse is sent off.

I also fixed an unintended error where i was overriding a flip-flop module's power to null.

With all of that resolved, I can run four iterations on the second example input...

...and get what I feel is the correct result: both flip-flop modules are off.

I have very little confidence that this program will produce the correct results on my example input.

But I'm ready to move on to cataloguing and counting pulses.

Counting high and low pulses

Two pulse types.

Eventually, multiplied together.

I'll use a 2-element array.

It should be easy to update the array inside my while loop.

I also need to track when the button is pushed.

I hope that's also as easy as it seems.

...

I added the necessary increment expressions.

I ran the program on the first example a single iteration.

I saw [8, 4]. Woohoo!

Running 1000 times correctly generates [8000, 4000]. Woohoo!

Running 1000 times on the second input generates the wrong answer.

Low pulses is off by 500. High pulses is off by 1000.

Bummer.

...

Wait a second.

The sum of the pulses is 7000.

Divided by 1000 is 7.

So, 7 pulses each time?

No...there are different amounts of pulses in each four-button cycle.

...

Oh ya! The output module!

My program doesn't account for pulses sent to that module.

I bet that's what I'm missing!

If I look at the number of pulses in a single iteration, I count three low and three high.

There are eight total. Two go to output: one high and one low.

So my counts are correct...they just neglect that unprefixed module!

I'd wager my input doesn't contain any unprefixed modules.

Therefore, maybe my code already works on my puzzle input???

Worth a try!

Wrong answer.

Too low.

Bummer.

I'm so over this challenge.

My brain hurts so bad.

And I don't see how to easily update my code to account for the small task of seeing the correct pulse counts for the second example.

Sadly, I leave with no new stars earned.

This one was just too darn confusing, I guess.

Top comments (0)