6 votes

Day 20: Pulse Propagation

Today's problem description: https://adventofcode.com/2023/day/20

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

8 comments

  1. RheingoldRiver
    Link
    I'm enjoying this less and less. If I were doing this myself I would've stopped today I think, but my friend was like "omg wtf" when I said this (and tbh at that point I was like 90% done with...

    I'm enjoying this less and less. If I were doing this myself I would've stopped today I think, but my friend was like "omg wtf" when I said this (and tbh at that point I was like 90% done with part 1 anyway) so I just went ahead and finished it.

    Actually I liked part 2 a lot more than part 1, I did it with a lot of manual checking stuff and not a pure programming solution, which is my preferred type of puzzle.

    Python solutions

    2 votes
  2. first-must-burn
    (edited )
    Link
    Golang solution This one was pretty tricky (Part 2, at least). I agree with @lily, the fact that the solution is not a general one but a property of the input bugs me. It took me a long time, but...

    Golang solution
    This one was pretty tricky (Part 2, at least). I agree with @lily, the fact that the solution is not a general one but a property of the input bugs me. It took me a long time, but I'm pleased that I solved it without any hints.

    Edited to add: When I brute force something and see that it's probably not going to solve in a reasonable time, I usually leave it running in a terminal just in case the slow algorithm completes faster than the time it takes me to implement some optimization. I think this got me one of the answers around day 7. I checked my progress against my solution as I was cleaning things up and it would only have taken 87 years to get today's solution the long way.

    Discussion

    I used dot to visualize the input, so I could recognize that they were LFSR-like counters. I wasn't sure if they would have a simple cycle, so I output some of the sequences to CSV to grub through manually. Once I figured out the cycles part, the rest was pretty straightforward.

    2 votes
  3. lily
    (edited )
    Link
    Tricky part 2 today. I honestly wasn't the hugest fan of today's problem, since the solution ended up being dependent on a quirk in the input, and so it took me longer to figure out than it...

    Tricky part 2 today. I honestly wasn't the hugest fan of today's problem, since the solution ended up being dependent on a quirk in the input, and so it took me longer to figure out than it probably should have.

    Solution
    # Advent of Code 2023
    # Day 20: Pulse Propagation
    
    from math import lcm
    
    class FlipFlop:
        def __init__(self, destinations):
            self.destinations = destinations
            self.on = False
    
        def recieve(self, name, pulse):
            if pulse == "low":
                self.on = not self.on
    
                if self.on:
                    return [(name, "high") for name in self.destinations]
                else:
                    return [(name, "low") for name in self.destinations]
            else:
                return []
    
    class Conjunction:
        def __init__(self, destinations):
            self.destinations = destinations
        
        def init_memory(self, connected_modules):
            self.memory = {name: "low" for name in connected_modules}
    
        def recieve(self, name, pulse):
            self.memory[name] = pulse
    
            if all(pulse == "high" for pulse in self.memory.values()):
                return [(name, "low") for name in self.destinations]
            else:
                return [(name, "high") for name in self.destinations]
    
    modules = {}
    conjunctions = {}
    
    with open("inputs/day_20.txt") as file:
        for line in file:
            if line.startswith("broadcaster"):
                starting_destinations = (
                    line.split(" -> ")[1].rstrip("\n").split(", ")
                )
            else:
                halves = line.split(" -> ")
                kind = (FlipFlop, Conjunction)[halves[0][0] == "&"]
                name = halves[0][1:]
                destinations = halves[1].rstrip("\n").split(", ")
    
                modules[name] = kind(destinations)
    
                if kind == Conjunction:
                    conjunctions[name] = modules[name]
    
                if "rx" in destinations:
                    final_conjunction = modules[name]
    
    for conjunction_name, conjunction in conjunctions.items():
        connected_modules = []
    
        if conjunction_name in starting_destinations:
            connected_modules.append("broadcaster")
    
        for name, module in modules.items():
            if conjunction_name in module.destinations:
                connected_modules.append(name)
    
        conjunction.init_memory(connected_modules)
    
    low_pulses = 1000
    high_pulses = 0
    
    presses = 1
    cycles = {name:None for name in final_conjunction.memory}
    
    while presses <= 1000 or any(cycle is None for cycle in cycles.values()):
        pulses = [("broadcaster", name, "low") for name in starting_destinations]
    
        while pulses:
            next_pulses = []
    
            for pulse in pulses:
                if (
                    pulse[0] in cycles and pulse[2] == "high"
                    and cycles[pulse[0]] is None
                ):
                    cycles[pulse[0]] = presses
    
                if pulse[1] in modules:
                    for next_pulse in modules[pulse[1]].recieve(
                        pulse[0], pulse[2]
                    ):
                        next_pulses.append((pulse[1],) + next_pulse)
    
                if presses <= 1000:
                    if pulse[2] == "low":
                        low_pulses += 1
                    else:
                        high_pulses += 1
    
            pulses = next_pulses
    
        presses += 1
    
    print(f"Part 1: {low_pulses * high_pulses}")
    print(f"Part 2: {lcm(*cycles.values())}")
    
    1 vote
  4. [5]
    DataWraith
    Link
    Well, today is where I jump ship. Part 1 was a lot of fun, and I was done quickly, but part 2 was too hard for me to solve within five hours. :( I guess I could look up the solution, but I think...

    Well, today is where I jump ship. Part 1 was a lot of fun, and I was done quickly, but part 2 was too hard for me to solve within five hours. :(

    I guess I could look up the solution, but I think that defeats the purpose, so I'll just give up here.

    Thoughts

    Part 1 was fun, but I have no idea how to solve part 2. Made a visualization of my graph, which shows several nasty cycles, and noticed that every second button press seems to repeat the same sequence for my input, but none of my attempts to suss out any regularity besides that came to fruition. Tried to see if there are any cycles for sub-graphs, but that didn't work, and I made all kinds of crazy guesses -- up to and including importing a SAT solver library to try and work backwards from the output before taking a step back and realizing that that can't possibly be required....

    Part 1 (Rust)
    pub fn part1(input: PuzzleInput) -> isize {
        let mut sim = CircuitSim::new(input);
        let mut low_pulses = 0;
        let mut high_pulses = 0;
    
        for i in 0..1000 {
            sim.button();
    
            while sim.tick() {
                // Nothing to do here
            }
    
            let pulses = sim
                .pulse_sequence
                .iter()
                .map(|(_, pulse, _)| pulse)
                .collect::<Vec<_>>();
    
            low_pulses += pulses.iter().filter(|&pulse| **pulse == Pulse::Low).count();
            high_pulses += pulses
                .iter()
                .filter(|&pulse| **pulse == Pulse::High)
                .count();
        }
    
        (low_pulses * high_pulses) as isize
    }
    
    #[derive(Clone, Debug)]
    pub struct CircuitSim {
        pub modules: HashMap<String, Module>,
        pub queue: VecDeque<(String, Pulse, String)>,
        pub pulse_sequence: Vec<(String, Pulse, String)>,
        pub button_presses: usize,
    }
    
        pub fn tick(&mut self) -> bool {
            if self.queue.is_empty() {
                return false;
            }
    
            let (from, pulse, name) = self.queue.pop_front().unwrap();
    
            self.pulse_sequence
                .push((from.clone(), pulse.clone(), name.clone()));
    
            if let Some(module) = self.modules.get_mut(&name) {
                for output in &module.outputs {
                    match module.state {
                        ModuleState::Broadcaster => {
                            self.queue.push_back((
                                "broadcaster".to_string(),
                                pulse.clone(),
                                output.clone(),
                            ));
                        }
    
                        ModuleState::FlipFlop { ref mut on } => {
                            if pulse == Pulse::Low {
                                if *on {
                                    self.queue
                                        .push_back((name.clone(), Pulse::Low, output.clone()));
                                } else {
                                    self.queue
                                        .push_back((name.clone(), Pulse::High, output.clone()));
                                }
                            }
                        }
    
                        ModuleState::Conjunction {
                            memory: ref mut cur_memory,
                        } => {
                            cur_memory.insert(from.clone(), pulse.clone());
    
                            let pulse = if cur_memory.values().all(|pulse| pulse == &Pulse::High) {
                                Pulse::Low
                            } else {
                                Pulse::High
                            };
    
                            self.queue.push_back((name.clone(), pulse, output.clone()));
                        }
                    }
                }
    
                match module.state {
                    ModuleState::Broadcaster => {}
    
                    ModuleState::FlipFlop { ref mut on } => {
                        if pulse == Pulse::Low {
                            *on = !*on;
                        }
                    }
    
                    ModuleState::Conjunction { memory: ref mut m } => {}
                }
            }
    
            true
        }
    }
    
    1 vote
    1. [3]
      first-must-burn
      Link Parent
      FWIW, I don't think there's anything wrong with getting hints. Once I've satisfied myself that I'm not making any more progress, I usually do go looking to see if I'm even barking up the right...

      FWIW, I don't think there's anything wrong with getting hints. Once I've satisfied myself that I'm not making any more progress, I usually do go looking to see if I'm even barking up the right tree. I think a lot of these problems are "pattern matching", and if I don't know about the right pattern, I'm unlikely to derive to solution from first principles.

      However, the ones I get stuck on are good opportunity for me to learn, so I will usually take the time to implement the solution just so I can work all the way through it.

      I play a lot of chess puzzles on lichess.org (find the mate or find the best move), and I approach that the same way. If I look at it for a while and don't see the answer, I just get the solution and then usually go back and play through to make sure I understand why it was the best move and what I was missing.

      3 votes
      1. [2]
        DataWraith
        Link Parent
        Thank you for your kind words. I've slept on it for a night now, and realize that I probably was too harsh on myself with the "no spoilers" rule I had apparently made up for myself. After all,...

        Thank you for your kind words. I've slept on it for a night now, and realize that I probably was too harsh on myself with the "no spoilers" rule I had apparently made up for myself. After all, even the "wrong answer"-page links to the subreddit, so I guess looking for hints is legitimate.

        I also hadn't thought I could just continue with a star missing (thanks, @wycy), hence my huge disappointment. I knew the next day's puzzle automatically unlocks, but somehow I didn't think of just skipping this part 2. I legitimately thought I was out for the year. And while the puzzles can be frustrating, I genuinely enjoy working on them, even if it takes hours where other people spend minutes.

        3 votes
        1. first-must-burn
          Link Parent
          That's one thing I like about AoC -- there are so many ways to approach it, and you can define your own criteria for success. I find myself slightly jealous of people who finish these complex...

          That's one thing I like about AoC -- there are so many ways to approach it, and you can define your own criteria for success.

          I find myself slightly jealous of people who finish these complex problems in 8 minutes with 15 lines of incomprehensible (to me) code. But I realize that those solutions are almost completely divorced from the reality of how commercial code is (and should be, IMO) written. It's a niche skill more like studying chess openings or the mathlete competitions.

          I don't mean to say that there's anything wrong with it. That would be pretty sour grapes. It's very impressive.

          It's exciting when I am awake enough to start the puzzle (midnight my time) when it drops and see how quickly I can finish. Especially for the easier days. But ultimately, building up those niche code competition skills isn't what I how I want to spend my time. So I've made peace with the limitations (in terms of competition rank) of going at my own pace, thinking more about structure, readability, and code reuse, and being happy with my solutions and their timing on my own terms.

          2 votes
    2. wycy
      Link Parent
      FYI you can still do future problems without having finished today’s part 2. (Back in 2018 I didn’t realize this and used someone else’s solution to a part 2 in order to continue on subsequent...

      FYI you can still do future problems without having finished today’s part 2. (Back in 2018 I didn’t realize this and used someone else’s solution to a part 2 in order to continue on subsequent days without realizing this wasn’t necessary)

      2 votes