12 votes

Day 8: Haunted Wasteland

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

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>

23 comments

  1. [4]
    Hvv
    Link
    C++ I really disliked Part 2. Part 2 Gripes So the ultimate solution here is pretty simple, since it's just taking all the loop lengths and getting their LCM. However, there are so many edge cases...

    C++

    I really disliked Part 2.

    Part 2 Gripes

    So the ultimate solution here is pretty simple, since it's just taking all the loop lengths and getting their LCM.

    However, there are so many edge cases that are possible here that you couldn't logically arrive at a quick solution from the problem description and must analyze the input to see what's going on.

    For example, from what I can tell the following input is entirely valid:

    L
    
    11A = (11B, XXX)
    11B = (11Z, XXX)
    11Z = (11B, XXX)
    22A = (11A, 22Z)
    XXX = (XXX, XXX)
    

    You can check to see that in this case you never escape!

    There are more complex cases than that, though:

    L
    
    11A = (11B,XXX)
    11B = (11Z,XXX)
    11Z = (11A,XXX)
    22A = (22Z,XXX)
    22Z = (22B,XXX)
    22B = (22C,XXX)
    22C = (22A,XXX)
    XXX = (XXX,XXX)
    

    where the solution requires the Chinese Remainder Theorem to solve programmatically.

    And there are cases even worse than that...

    To make matters worse, they used the same LCM idea in 2019 and had the problem worded in such a way that you could arrive at the LCM solution logically and prove that it worked, rather than guessing at it from the specific input you're given. They got it right already! Was this really the best way to bring out this idea again?

    I get that sometimes hacking the input is its own type of cleverness, but this was a significant break from the convention of the other problems and there was no indication this would the right way to go.

    Part 1 Solution The problem here is straightforward, which is to just follow the path from AAA until it reaches ZZZ. Of course, that leaves the full implementation to you.

    Implementation notes:

    Aside from the almost comedically bad string parsing (even after cleaning up),
    there are a couple of dumb implementation details that I love,
    like converting a string to an int by just sticking the whole thing in a map or by dynamically resizing the array whenever we need extra elements.
    After parsing all the strings, it's relatively easy to get the answer out.

    If anyone has a better way to parse strings (that doesn't involve a library) I would be happy to hear it.
    Somehow I suspect this might not exist, because C++ tends to be the sort of language that makes you do that yourself.

    Part 1 Code
    #include <bits/stdc++.h>
    using namespace std;
    map<string,int> string_to_node;
    int get_node(const string& s) {
    	if(string_to_node.count(s)) {
    		return string_to_node[s];
    	} else {
    		int ans = string_to_node.size();
    		string_to_node[s] = ans;
    		return ans;
    	}
    }
    int main() {
    	string path_choices;
    	getline(cin,path_choices);
    	string input_line;
    	getline(cin,input_line);
    
    	vector<array<int,2>> left_right_paths;
    	getline(cin,input_line);
    
    	while(input_line.size() > 0) {
    
    		// Ah yes, bespoke string parsing in C++
    		stringstream ss(input_line);
    		string start,left_string,right_string,_;
    		ss >> start;
    		ss >> _;
    		ss >> left_string;
    		ss >> right_string;
    		// Take out the first character '(' and the last character ',' from the left
    		// Take out the last character ')' from the right
    		left_string = left_string.substr(1,left_string.size()-2);
    		right_string = right_string.substr(0,right_string.size()-1);
    
    		int node = get_node(start);
    		int left_node = get_node(left_string);
    		int right_node = get_node(right_string);
    
    		left_right_paths.resize(string_to_node.size());
    		left_right_paths[node][0] = left_node;
    		left_right_paths[node][1] = right_node;
    		getline(cin,input_line);
    	}
    
    	int node = get_node("AAA");
    	int end_node = get_node("ZZZ");
    
    	int len = 0;
    	while(node != end_node) {
    		int path = path_choices[len%path_choices.size()] == 'L'?0:1;
    		node = left_right_paths[node][path];
    		len++;
    	}
    	cout << len << '\n';
    }
    
    Part 2 Code
    #include <bits/stdc++.h>
    using namespace std;
    map<string,int> string_to_node;
    long long gcd(long long a, long long b) {
    	if(a == 0) {return b;}
    	if(b == 0) {return a;}
    	return gcd(b,a%b);
    }
    long long lcm(long long a, long long b) {
    	return b/gcd(a,b)*a;
    }
    
    int get_node(const string& s) {
    	if(string_to_node.count(s)) {
    		return string_to_node[s];
    	} else {
    		int ans = string_to_node.size();
    		string_to_node[s] = ans;
    		return ans;
    	}
    }
    int main() {
    	string path_choices;
    	getline(cin,path_choices);
    	string input_line;
    	getline(cin,input_line);
    
    	vector<array<int,2>> left_right_paths;
    	getline(cin,input_line);
    
    	while(input_line.size() > 0) {
    		stringstream ss(input_line);
    		string start,left_string,right_string,_;
    		ss >> start;
    		ss >> _;
    		ss >> left_string;
    		ss >> right_string;
    		left_string = left_string.substr(1,left_string.size()-2);
    		right_string = right_string.substr(0,right_string.size()-1);
    
    		int node = get_node(start);
    		int left_node = get_node(left_string);
    		int right_node = get_node(right_string);
    
    		left_right_paths.resize(string_to_node.size());
    		left_right_paths[node][0] = left_node;
    		left_right_paths[node][1] = right_node;
    		getline(cin,input_line);
    	}
    
    	set<int> z_suffix;
    	vector<int> a_suffix;
    	for(const auto& it: string_to_node) {
    		if(it.first.back() == 'Z') {
    			z_suffix.insert(it.second);
    		}
    		if(it.first.back() == 'A') {
    			a_suffix.push_back(it.second);
    		}
    	}
    
    	long long total = 1;
    	for(const auto& node: a_suffix) {
    		int len = 0;
    		int current_node = node;
    		while(!z_suffix.count(current_node)) {
    			int path = path_choices[len%path_choices.size()] == 'L'?0:1;
    			current_node = left_right_paths[current_node][path];
    			len++;
    		}
    		total = lcm(total,len);
    	}
    	cout << total << '\n';
    }
    
    4 votes
    1. [2]
      first-must-burn
      Link Parent
      In Go and Python, I get a lot of mileage out of string.split for parsing. In my Day 2 solution, I break the line down by first splitting on :, then splitting on ;, then on ,, then on spaces...

      If anyone has a better way to parse strings (that doesn't involve a library) I would be happy to hear it.

      In Go and Python, I get a lot of mileage out of string.split for parsing. In my Day 2 solution, I break the line down by

      I haven't written C++ in a while, so when I was looking to see what the string split capability was, I found that the Man Himself commented on token parsing at #5 in this article that it's only becomes neatly available in c++20.

      Especially if you are splitting on single tokens, it's pretty easy to implement the split yourself, which as you noted, is where C++ often leaves you with strings. I think this is a product of the era that produced C++ - much more concern about minimizing resource utilization and more focus on numeric computation than on UIs (which generally means more parsing and generating strings for human interactions).

      I'll also note that today's solution is fixed width, so you can just substr the values directly out the string without all the token parsing.

      Also, on your gripes about Part 2, I think AoC suffers a little bit from the fact that we know it's constructed to have a (relatively) simple solution. If there was something that would make it so that every solution was super-complicated or time-consuming to solve, they would have designed that out. This is the same thing I run into doing puzzles on lichess. I'm pretty good at them, but I'm not that good at regular chess because in the puzzle, I know there's a clever move I'm supposed to look for but in a game (as in real life coding) there is no such guarantee.

      4 votes
      1. Hvv
        (edited )
        Link Parent
        For part 1, I'll admit now that part of my hideous string parsing is what happens when some part of my brain goes "but you must think of the general case" but only for things like input string...

        For part 1,

        I'll admit now that part of my hideous string parsing is what happens when some part of my brain goes "but you must think of the general case" but only for things like input string length, leading to parsing ideas like "remove the last element which we know to be ")" but only reference the possibly non-fixed (even though we know it's fixed) string size when doing so".

        I was already doing some fiddling with stringstream but I didn't know about split_view, so I'll look into that. At first glance it looks like it only accepts one delimiter, but that might still be useful in the future if I have to deal with commas or some related non-whitespace thing.

        Part 2 discussion, with spoilers

        You're right that Advent of Code often simplifies the problem implicitly in order to allow for simpler solutions.

        To be more specific, my problem with Part 2 is that the simplified input is artificial to the point of absurdity.

        For contrast, in Day 5 the maps given in the input were bijective (I think) even though there was no indication that this would be true in the problem (and indeed it was easy to create a non-bijective map). However, this idea that those maps will be one-to-one is very intuitive and is clearly shown by the example input, so it felt like a reasonable assumption to make.

        In addition, there is a nice general solution that covers non-bijective maps. It's not as simple as the specialized solution, but it's still reasonable and thinking about it makes the problem more interesting.

        However, the simplest type of graphs I can think of where the intended solution works is...

        "Graphs in which the first node is not in a cycle, but immediately enters a cycle where, looking at the state graph, only the last element in the cycle (before looping) has Z as a suffix"

        Or (closely related but not quite the same):

        "Graphs in which the first step from the first node enters a cycle whose length is a multiple of the instruction length and only the last element in the cycle has Z as a suffix"

        (There are other graphs for which this works but I think they're even more complicated to describe)

        This is plainly ridiculous. To steal your chess analogy, it's like seeing a move and thinking "oh, this tactic would work if only they took with the queen, but obviously they won't because they can just block the threat with another piece". Except the tactic maker does intend have them take with the queen, because only then could the tactic feature a Windmillwe're really stretching the analogy here you get the point.

        I was going to say that upon writing this I realized that the example input follows that description, but it doesn't! The 22A cycle has period 3 (the instructions are LR, length 2), clearly only doing so because all the L/R choices are the same.

        Note on the example input

        The example input doesn't present a simpler class of graphs, because the state after 1 move is 22B to move Right but the state after 4 moves is 22B to move Left. It's entirely possible for you to arrive at the same node but take a different path because you're at a different point in the instruction set. It only happens to be true here because L/R are the same everywhere, and we would need to add even more conditions to the graph if we wanted to quantify that.

        In addition, there are several very similar classes of graphs with different solutions.

        For example, removing the requirement of "the node with Z is at the end of the loop" adds in the need for the Chinese Remainder Theorem. Removing "only one node with Z in the cycle" adds a ton of complexity. Even changing the janky "the first node is not in a cycle but its first move enters the cycle" to "the first node is the first node of the cycle" will at least require the solution to subtract one, but they didn't even do that.

        Advent of code does often simplify its problem sets, and most of the time it does so pretty well. But this one feels like it used "simplify the input" to paper over bad problem design.

        2 votes
    2. ra314
      Link Parent
      I agree with your gripes. But if you've done the previous years of Advent of Code, you'll see there is (IMO) at least 1 problem every year, where making certain assumptions about the input allow...

      I agree with your gripes. But if you've done the previous years of Advent of Code, you'll see there is (IMO) at least 1 problem every year, where making certain assumptions about the input allow you to solve the problem in a trivial amount of time. That wouldn't work for a general arbitrary input.

      I dislike it as well but it's not unexpected for Advent of Code.

      2 votes
  2. [4]
    RheingoldRiver
    Link
    Easily my favorite day so far. spoiler part 2 I'm still using Python 3.8 which doesn't have `math.lcm` built in, so I tried using numpy, but that gave me the wrong answer. So...I printed the...

    Easily my favorite day so far.

    spoiler part 2 I'm still using Python 3.8 which doesn't have `math.lcm` built in, so I tried using numpy, but that gave me the wrong answer. So...I printed the numbers and fed it into Wolfram Alpha and that was right. Gonna update to 3.11 so it works out better after this.

    Solutions:

    Part 1
    import json
    import re
    from copy import copy, deepcopy
    
    
    class Solver:
        start = 'AAA'
        end = 'ZZZ'
    
        def __init__(self):
            with open('info.json', 'r', encoding='utf-8') as f:
                self.data = json.load(f)
            self.directions = self.data["line1"] * 20000
            self.resulting_options = [self.parse_line(line) for line in self.data["lookup"].split('\n')]
            self.lookup = {item['start']: item for item in self.resulting_options}
    
        @staticmethod
        def parse_line(line):
            start = line.split(' = ')[0]
            results = line.split(' = ')[1].replace('(', '').replace(')', '')
            return {
                'start': start,
                'L': results.split(', ')[0],
                'R': results.split(', ')[1],
            }
    
        def run(self):
            total = 0
            cur = self.start
            for char in self.directions:
                total += 1
                cur = self.lookup[cur][char]
                if cur == self.end:
                    break
    
            return total
    
    
    if __name__ == '__main__':
        print(Solver().run())
    
    Part 2
    import json
    import math
    import re
    import numpy as np
    
    
    class Solver:
        start = 'AAA'
        end = 'ZZZ'
    
        def __init__(self):
            with open('info.json', 'r', encoding='utf-8') as f:
                self.data = json.load(f)
            self.directions = self.data["line1"] * 2000000
            self.resulting_options = [self.parse_line(line) for line in self.data["lookup"].split('\n')]
            self.lookup = {item['start']: item for item in self.resulting_options}
    
        @staticmethod
        def parse_line(line):
            start = line.split(' = ')[0]
            results = line.split(' = ')[1].replace('(', '').replace(')', '')
            return {
                'start': start,
                'L': results.split(', ')[0],
                'R': results.split(', ')[1],
            }
    
        def run(self):
            total = 0
            cur = [loc for loc in self.lookup.keys() if loc.endswith('A')]
            prime_factors = [None] * len(cur)
            for char in self.directions:
                total += 1
                for i, loc in enumerate(cur):
                    new_square = self.lookup[loc][char]
                    cur[i] = new_square
                    if new_square.endswith('Z') and prime_factors[i] is None:
                        prime_factors[i] = total
                print(prime_factors)
                if all(x is not None for x in prime_factors):
                    break
            print(prime_factors)
    
            return np.lcm.reduce(prime_factors)
    
    
    if __name__ == '__main__':
        print(Solver().run())
    
    3 votes
    1. [2]
      tjf
      Link Parent
      Didn't realize that was missing from the stdlib for so long, but thankfully the version of PyPy I'm using has it. Edit: Are the 2000 and 2000000 multipliers you use just arbitrary large numbers?...

      Didn't realize that was missing from the stdlib for so long, but thankfully the version of PyPy I'm using has it.

      Edit: Are the 2000 and 2000000 multipliers you use just arbitrary large numbers? If you look at my solutions there's a neat Python-specific trick for this.

      2 votes
      1. RheingoldRiver
        Link Parent
        yep just large numbers. that's a neat built-in, thanks!

        yep just large numbers. that's a neat built-in, thanks!

        2 votes
    2. scarecrw
      (edited )
      Link Parent
      You can also get around that particular omission with a neat identity! spoilers lcm = lambda a, b: a * b // gcd(a, b)

      You can also get around that particular omission with a neat identity!

      spoilers
      lcm = lambda a, b: a * b // gcd(a, b)
      
      2 votes
  3. tjf
    Link
    Today's problem reminded me of 2022's Day 17, which was a good one! Here are my Python solutions: Part 1 Python's itertools.cycle is really useful here. #!/usr/bin/env pypy3 import itertools...

    Today's problem reminded me of 2022's Day 17, which was a good one! Here are my Python solutions:

    Part 1

    Python's itertools.cycle is really useful here.

    #!/usr/bin/env pypy3
    
    import itertools
    import sys
    
    seq = tuple(int(c == 'R') for c in sys.stdin.readline().strip())
    sys.stdin.readline()
    moves = {}
    for line in sys.stdin:
        k, v = map(str.strip, line.split('='))
        moves[k] = tuple(side.strip('()') for side in v.split(', '))
    
    position = 'AAA'
    steps = 0
    for move in itertools.cycle(seq):
        if position == 'ZZZ':
            break
        position = moves[position][move]
        steps += 1
    
    print(steps)
    

    Let my CPU cook for a few minutes before turning on my brain and thinking. I got to break out my main for loop into a generator function which is always nice.

    Part 2
    #!/usr/bin/env pypy3
    
    import itertools
    import math
    import sys
    
    def nsteps(seq, moves, positions):
        for p in positions:
            steps = 0
            for move in itertools.cycle(seq):
                if p[-1] == 'Z':
                    break
                p = moves[p][move]
                steps += 1
            yield steps
    
    seq = [int(c == 'R') for c in sys.stdin.readline().strip()]
    sys.stdin.readline()
    moves = {}
    for line in sys.stdin:
        k, v = map(str.strip, line.split('='))
        moves[k] = tuple(side.strip('()') for side in v.split(', '))
    
    positions = (p for p in moves if p[-1] == 'A')
    steps = nsteps(seq, moves, positions)
    print(math.lcm(*steps))
    
    3 votes
  4. lily
    Link
    Really fun problem, and felt just about the right level of difficulty for this far into the month. The solution was clever, but not too hard to figure out - I like solutions like that, since they...

    Really fun problem, and felt just about the right level of difficulty for this far into the month. The solution was clever, but not too hard to figure out - I like solutions like that, since they make me feel smart ;)

    I solved the problem a while before posting this, but took some time to assemble a nice program that solves both parts at once and to write actual input parsing (I initially just copied the input into my program and used a regex to convert it to a Python dictionary). I got top 400 on part 1, but unfortunately wasn't quite so fast doing part 2.

    (Also, yes, I know the input parsing is horrible. It's just Advent of Code though, it's fine I promise)

    Solution
    # Advent of Code 2023
    # Day 8: Haunted Wasteland
    
    import math
    
    graph = {}
    
    with open("inputs/day_8.txt") as file:
        for i, line in enumerate(file):
            if i == 0:
                instructions = line.rstrip("\n")
            elif i > 1:
                graph[line[:3]] = (line[7:10], line[12:15])
    
    def next_location(graph, location):
        return graph[location]["LR".index(instructions[steps % len(instructions)])]
    
    steps = 0
    location = "AAA"
    
    while location != "ZZZ":
        location = next_location(graph, location)
        steps += 1
    
    print(f"Part 1: {steps}")
    
    locations = [location for location in graph if location[-1] == "A"]
    steps_per_location = []
    
    for i in range(len(locations)):
        steps = 0
        z_count = 0
    
        while z_count < 2:
            locations[i] = next_location(graph, locations[i])
            steps += 1
    
            if locations[i][-1] == "Z":
                if z_count == 0:
                    steps = 0
    
                z_count += 1
    
        steps_per_location.append(steps)
    
    print(f"Part 2: {math.lcm(*steps_per_location)}")
    
    3 votes
  5. whs
    Link
    I tried to learn Ruby over half a decade ago, but I found that it mostly overlap Python in use cases so I didn't get to use Ruby much and forgot almost everything about it. Looking at the problem...

    I tried to learn Ruby over half a decade ago, but I found that it mostly overlap Python in use cases so I didn't get to use Ruby much and forgot almost everything about it. Looking at the problem it seems doable in Ruby even though I don't know how to write anything in Ruby now. Surprisingly, I think this is one of the fastest day I've completed so far.

    Part 1
    instruction = gets.chomp
    gets
    
    nodes = {}
    
    while l = gets do
    	parsed = l.match(/^([A-Z]+) = \(([A-Z]+), ([A-Z]+)\)/)
    	nodes[parsed[1]] = {
    		"L" => parsed[2],
    		"R" => parsed[3],
    	}
    end
    
    current = "AAA"
    step = 0
    
    while current != "ZZZ" do
    	cmd = instruction[step % instruction.length]
    	current = nodes[current][cmd]
    	step += 1
    end
    
    puts step
    
    Part 2
    instruction = gets.chomp
    gets
    
    $nodes = {}
    
    while l = gets do
    	parsed = l.match(/^([0-9A-Z]+) = \(([0-9A-Z]+), ([0-9A-Z]+)\)/)
    	$nodes[parsed[1]] = {
    		"L" => parsed[2],
    		"R" => parsed[3],
    	}
    end
    
    current = $nodes.keys.filter { |k| k.end_with?("A") }
    
    current_z_every = current.map { |v| 
    	step = 0
    	cur = v
    	while !cur.end_with?("Z") do
    		cmd = instruction[step % instruction.length]
    		cur = $nodes[cur][cmd]
    		step += 1
    	end
    	step
    }
    
    puts current_z_every.reduce(1, :lcm)
    

    I don't like math problem posing as programming problem...

    2 votes
  6. [3]
    pnutzh4x0r
    Link
    Language: Python Part 1 First part was very straight-forward: read the input in and simulate the navigation. Taking advantage of itertools.cycle made cycling through the instructions very easy :]...

    Language: Python

    Part 1

    First part was very straight-forward: read the input in and simulate the navigation. Taking advantage of itertools.cycle made cycling through the instructions very easy :]

    Network = dict[str, dict[str, str]]
    
    def read_instructions(stream=sys.stdin) -> Iterator[str]:
        return itertools.cycle(stream.readline().strip())
    
    def read_network(stream=sys.stdin) -> Network:
        network = defaultdict(dict)
    
        for line in map(str.strip, stream):
            if not line:
                continue
    
            source, target_l, target_r = re.findall('[A-Z]{3}', line)
            network[source] = {
                'L': target_l,
                'R': target_r,
            }
    
        return network
    
    def navigate(instructions: Iterator[str], network: Network) -> int:
        count  = 0
        source = 'AAA'
        target = 'ZZZ'
    
        while source != target:
            source = network[source][next(instructions)]
            count += 1
    
        return count
    
    def main(stream=sys.stdin) -> None:
        instructions = read_instructions(stream)
        network      = read_network(stream)
        print(navigate(instructions, network))
    
    Part 2

    The second part was also straight-forward: locate the ghosts, and then navigate from each ghost until you hit an element that ends with a 'Z'. The trick to avoid brute-forcing is to realize that the ghosts will cycle (ie. repeat the same paths) if they all don't land on an element that ends with a 'Z' at the same time. To solve the problem, you just ened to calculate the steps for each ghost to reach an endpoint and then compute the lowest common multiple of those counts. Fortunately, Python has math.lcm, so not much code was needed!

    def navigate(instructions: Iterator[str], network: Network, element: str) -> int:
        count = 0
    
        while not element.endswith('Z'):
            element = network[element][next(instructions)]
            count += 1
    
        return count
    
    def locate_ghosts(network: Network) -> list[str]:
        return [element for element in network if element.endswith('A')]
    
    def main(stream=sys.stdin) -> None:
        instructions = read_instructions(stream)
        network      = read_network(stream)
        ghosts       = locate_ghosts(network)
        counts       = [navigate(cycle(instructions), network, ghost) for ghost in ghosts]
        print(math.lcm(*counts))
    

    GitHub Repo

    2 votes
    1. [2]
      DataWraith
      Link Parent
      Re: Part 2 Question Oh wow, that's so much simpler than what I did. I'm not so good at math, so I'm having trouble understanding why you can stop at the first node that ends in 'Z', which you're...

      Re: Part 2

      Question

      Oh wow, that's so much simpler than what I did.

      I'm not so good at math, so I'm having trouble understanding why you can stop at the first node that ends in 'Z', which you're doing if I'm not reading the Python code wrongly.

      What if there are multiple Z-nodes in the cycle, and you don't happen to need to be stopped at the first one, but, say, at the second one, when all other paths terminate? I reckon there's probably a mathematical reason for this case being impossible, but right now it seems weird to me. Can you enlighten me?

      1 vote
      1. pnutzh4x0r
        Link Parent
        Response Honestly, I'm not sure if there is a mathematical reason (at least I didn't have one in mind). I sort have just assumed that once it hits a Z-node that it cycles through the same...
        Response

        Honestly, I'm not sure if there is a mathematical reason (at least I didn't have one in mind). I sort have just assumed that once it hits a Z-node that it cycles through the same sequence. If this assumption didn't hold, then you are right... my solution would not always work.

        So perhaps better lucky than good :]

        2 votes
  7. first-must-burn
    Link
    First a joke: What's a Go programmer's favorite fruit? main.go (in my defense, it is late and I am tired). Golang solution Very pleasing escalation from p1 to p2. I spent some spare time today...

    First a joke:

    What's a Go programmer's favorite fruit?

    main.go

    (in my defense, it is late and I am tired).

    Golang solution

    Very pleasing escalation from p1 to p2. I spent some spare time today building a general tree node structure in prep for tree problems. So I was bummed about it being a graph problem. However, the graph for this was pretty simple to implement. I still think there will be at least one DFS problem so I'm ready with my shiny new hammer.

    Spoilers part 2

    I should not have wasted time on the naïve solution (doing the walk of the graph with all of the starts simultaneously, instead of doing them one by one and finding the LCM). I should know by now to look for the shortcut first.

    1 vote
  8. scarecrw
    (edited )
    Link
    I feel like I got a bit lucky that my first guess at part 2 worked. Spoilers Not looking at the input, I was worried there could have been cycles with multiple `XXZ` patterns at different periods,...

    I feel like I got a bit lucky that my first guess at part 2 worked.

    Spoilers Not looking at the input, I was worried there could have been cycles with multiple `XXZ` patterns at different periods, or offsets from starting in an initial state not included in the cycle. I know in at least one year a challenge required bringing out the Chinese remainder theorem.

    This day also got me to finally start looking at different data structures in Haskell. I found Data.Map, but was surprised to find it has lookup of O(log(n)). More than enough for today, but interesting nonetheless.

    Haskell Solution
    module Main (main) where
    import Prelude hiding (lookup)
    import Data.Map (Map, fromList, lookup, keys)
    
    main :: IO ()
    main = do
        input <- readFile "./input.txt"
        putStrLn $ "Part 1: " ++ show (uncurry solve1 $ parseInput input)
        putStrLn $ "Part 2: " ++ show (uncurry solve2 $ parseInput input)
    
    parseInput :: String -> (String, Map String (String, String))
    parseInput input = (steps, mapping) where
        steps = head $ lines input
        mappingStrs = drop 2 $ lines input
        mapping = fromList $ map parseLine mappingStrs
    
    parseLine :: String -> (String, (String, String))
    parseLine line = (a, (b, c)) where
        a = take 3 line
        b = take 3 $ drop 7 line
        c = take 3 $ drop 12 line
    
    nextNode :: Map String (String, String) -> String -> Char -> String
    nextNode mapping curr direction = case lookup curr mapping of
        Nothing -> error "key not found"
        Just (l, r) -> case direction of
            'L' -> l
            'R' -> r
            _   -> error "invalid step"
    
    solve :: Map String (String, String) -> String -> (String -> Bool) -> [Char] -> Int
    solve mapping steps endCond start = solveRec (cycle steps) start 0 where
        solveRec steps curr n
            | endCond curr = n
            | otherwise = solveRec (tail steps) (nextNode mapping curr (head steps)) (n+1)
    
    solve1 :: String -> Map String (String, String) -> Int
    solve1 steps mapping = solve mapping steps (=="ZZZ") "AAA"
    
    solve2 :: String -> Map String (String, String) -> Int
    solve2 steps mapping = foldr lcm 1 cycleLengths where
        aStarts = filter ((=='A') . (!!2)) $ keys mapping
        cycleLengths = map (solve mapping steps ((=='Z') . (!!2))) aStarts
    

    I'm sure I'm still doing plenty of non-idiomatic goofiness, but that's what trying new things is for!

    1 vote
  9. DataWraith
    Link
    (Rust) Thoughts Well, that was fun. I tried to brute-force Part 2 at first, with puzzles being brute-forceable being a theme of the year so far, but nope. I hate it when Part 2 asks you to do...

    (Rust)

    Thoughts

    Well, that was fun. I tried to brute-force Part 2 at first, with puzzles being brute-forceable being a theme of the year so far, but nope.

    I hate it when Part 2 asks you to do something in parallel, like the "release steam from vents together with an Elephant"-puzzle in one of the last years, because depending on how you structured part 1, that can be really difficult to rework. Thankfully that wasn't really the case for me here.

    Part 1

    (nom-Parser omitted for brevity)

    pub struct PuzzleInput {
        pub instructions: &'static str,
        pub nodes: BTreeMap<&'static str, (&'static str, &'static str)>,
    }
    
    impl PuzzleInput {
        pub fn instructions(&self) -> impl Iterator<Item = char> {
            self.instructions.chars().cycle()
        }
    
        pub fn path_to_end(&self, start: &'static str) -> impl Iterator<Item = &str> {
            let mut current = start;
            let mut instrs = self.instructions();
    
            std::iter::once(current).chain(std::iter::from_fn(move || {
                if current == "ZZZ" {
                    return None;
                }
    
                let (left, right) = self.nodes.get(current).unwrap();
                let go_left = instrs.next().unwrap() == 'L';
                current = if go_left { left } else { right };
                Some(current)
            }))
        }
    }
    
    pub fn part1(input: &'static str) -> usize {
        let parsed = parser::parse(input).expect("Failed to parse input").1;
        parsed.path_to_end("AAA").count() - 1
    }
    
    Part 2

    The algorithm is simple once you figured it out: find all cycles, compute the indices on which a given path arrives at a Z-node, and then find the least common multiple between all paths, because that is the first step at which all paths will land on a Z-node.

    pub fn part2(input: &'static str) -> usize {
        let parsed = parser::parse(input).expect("Failed to parse input").1;
    
        find_paths(parsed)
    }
    
    #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
    pub struct GraphNode {
        node: &'static str,
        index: usize,
    }
    
    fn find_paths(input: PuzzleInput) -> usize {
        fn single_loop(input: &PuzzleInput, from: &'static str) -> (usize, Vec<GraphNode>) {
            let instr_len = input.instructions.len();
    
            let mut current = from;
            let mut instrs = input.instructions();
            let mut index = 0;
    
            let mut result = VecDeque::new();
            let mut seen = BTreeSet::new();
    
            let mut node = GraphNode {
                node: current,
                index,
            };
    
            loop {
                result.push_back(node.clone());
    
                if !seen.insert(node.clone()) {
                    cycle_start = node;
                    break;
                }
    
                let go_left = instrs.next().unwrap() == 'L';
    
                if go_left {
                    let (left, _) = input.nodes.get(current).unwrap();
                    current = left;
                } else {
                    let (_, right) = input.nodes.get(current).unwrap();
                    current = right;
                }
    
                index = (index + 1) % instr_len;
    
                node = GraphNode {
                    node: current,
                    index,
                };
            }
    
            let mut cycle_starts_at = 0;
            while result.front() != Some(&cycle_start) {
                result.pop_front();
                cycle_starts_at += 1;
            }
    
            result.pop_back();
    
            (cycle_starts_at, result.into())
        }
    
        fn cycle_times(cycle: &Vec<GraphNode>, cycle_starts_at: usize) -> Vec<usize> {
            let mut result = Vec::new();
    
            for (i, node) in cycle.iter().enumerate() {
                if node.node.ends_with('Z') {
                    result.push(cycle_starts_at + i);
                }
            }
    
            result
        }
    
        fn gcd(a: usize, b: usize) -> usize {
            let mut a = a;
            let mut b = b;
    
            while b != 0 {
                let t = b;
                b = a % b;
                a = t;
            }
    
            a
        }
    
        fn lcm(a: usize, b: usize) -> usize {
            a * b / gcd(a, b)
        }
    
        let loop_lengths = input
            .nodes
            .keys()
            // Find all starting nodes
            .filter(|node| node.ends_with('A'))
            // Determine how the path loops
            .map(|node| single_loop(&input, node))
            // Calculate the time indices where a path is at a Z-node
            .map(|(cycle_starts_at, cycle)| cycle_times(&cycle, cycle_starts_at))
            .collect::<Vec<_>>();
    
        // There must be a more elegant way to compute the least-common multiple
        // of a Vector of indeterminate length...
        (loop_lengths.into_iter().fold(BTreeSet::new(), |acc, x| {
            let mut result = BTreeSet::new();
    
            if acc.is_empty() {
                for x in x.iter() {
                    result.insert(*x);
                }
                return result;
            }
    
            for a in acc.iter() {
                for b in x.iter() {
                    result.insert(lcm(*a, *b));
                }
            }
    
            result
        }))
        .into_iter()
        // Lowest common multiple
        .min()
        .unwrap()
    }
    
    1 vote
  10. jonah
    Link
    I finally got around to day 8 tonight. I'm only posting part 2 since it's really similar to part 1 and I'm really happy that I figured out... Spoilers that it was an LCM problem. I'm usually very...

    I finally got around to day 8 tonight. I'm only posting part 2 since it's really similar to part 1 and I'm really happy that I figured out...

    Spoilers that it was an LCM problem. I'm usually very bad at figuring these types of things out. And on the plus side I can add the `gcd` and `lcm` code to my utils :D

    Parsing was also pretty easy with some simple regex today, which is very welcome. Okay, here's my code

    Part 2
    import { getInput } from "./input";
    import * as utils from "./utils";
    
    const gcd = (a: number, b: number): number => {
      return !b ? a : gcd(b, a % b);
    };
    
    const lcm = (a: number, b: number): number => {
      return (a * b) / gcd(a, b);
    };
    
    const alcm = (nums: number[]): number => {
      nums.sort((a, b) => a - b);
      while (nums.length > 1) {
        const x = lcm(nums[0], nums[1]);
        nums.splice(0, 2, x);
      }
      return nums[0];
    };
    
    (async () => {
      const input = getInput();
      const lines = input.split("\n").filter((l) => !!l);
    
      const dirs = lines[0].split("");
      const nodes: Map<string, [string, string]> = new Map();
      const regex = /(\w+) = \((\w+), (\w+)\)/;
    
      for (let i = 1; i < lines.length; i++) {
        const args = lines[i].match(regex)!;
        nodes.set(args[1], [args[2], args[3]]);
      }
    
      const keys = [...nodes.keys()].filter((x) => x.endsWith("A"));
      console.log(`Going through ${keys.length} paths`);
      const steps = keys.map((key, i) => {
        let count = 0;
        let currNode = key;
        while (currNode != "ZZZ") {
          const lr = nodes.get(currNode)!;
          const dir = dirs[count % dirs.length] === "L" ? 0 : 1;
          currNode = lr[dir];
          count++;
          if (currNode.endsWith("Z")) {
            break;
          }
        }
        console.log(`Finished ${i + 1}`);
        return count;
      });
    
      console.log(alcm(steps));
    })();
    

    Because I didn't brute force the problem I was able to get the solution in less than a second which was like an early Christmas for me! Reading through some of the comments here it seems like today's part 2 is maybe controversial. Personally I thought it was fun, but I also lucked out a little bit with some assumptions and figuring out the secret earlier than usual.

    1 vote
  11. infinitesimal
    Link
    I tried to do part 2 naively at first and when that worked on the example but not the input I tried to do the next simplest thing which then happened to work, but upon reading other comments I...

    I tried to do part 2 naively at first and when that worked on the example but not the input I tried to do the next simplest thing which then happened to work, but upon reading other comments I realized it wasn't "supposed" to work except the input was specially crafted for it to work.

    Kotlin
    object Day08 {
        fun parse(input: String): Pair<String, Map<String, Pair<String, String>>> {
            val (path0, graph0) = input.split("\n\n")
            val path = path0.trim()
            val graph = mutableMapOf<String, Pair<String, String>>()
            for (line in graph0.lines()) {
                val (node, left, right) = alnumsOf(line).toList()
                graph[node] = Pair(left, right)
            }
            return Pair(path, graph)
        }
    
        fun cycleOf(path: String) = iterator {
            var i = 0
            while (true) {
                yield(path[i])
                i = (i + 1) % path.length
            }
        }
    
        fun stepsOf(node: String, path: String, graph: Map<String, Pair<String, String>>): Int {
            val cycle = cycleOf(path)
            var numSteps = 0
            var location = node
            while (!location.endsWith('Z')) {
                location = when (cycle.next()) {
                    'L' -> graph[location]!!.first
                    'R' -> graph[location]!!.second
                    else -> error(Unit)
                }
                numSteps++
            }
            return numSteps
        }
    
        fun part1(input: String): Int {
            val (path, graph) = parse(input)
            return stepsOf("AAA", path, graph)
        }
    
        fun part2(input: String): Long {
            val (path, graph) = parse(input)
            val starts = graph.keys.filter { it.endsWith('A') }
            val steps = starts.map { stepsOf(it, path, graph).toLong() }
            return steps.reduce { x, y -> lcm(x, y) }
        }
    }
    
    1 vote
  12. wycy
    Link
    Rust Day 8 use std::env; use std::io::{self, prelude::*, BufReader}; use std::fs::File; use std::collections::HashMap; extern crate num; use num::integer; extern crate regex; use regex::Regex;...

    Rust

    Day 8
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    use std::collections::HashMap;
    
    extern crate num;
    use num::integer;
    
    extern crate regex;
    use regex::Regex;
    
    #[derive(Debug)]
    struct Node {
        name: String,
        left: String,
        right: String,
    }
    impl From<&String> for Node {
        fn from(s: &String) -> Node {
            let (name,left,right) = node_values(&s);
            Self {
                name:  name,
                left:  left,
                right: right,
            }
        }
    }
    
    fn node_values(s: &String) -> (String,String,String) {
        let re = Regex::new(r"^(\w+) = \((\w+), (\w+)\)$").unwrap();
        let matches = re.captures(s).unwrap();
        (matches[1].to_string(),
         matches[2].to_string(),
         matches[3].to_string())
    }
    
    fn solve(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
                Err(err) => panic!("Unknown error reading input: {}", err),
                Ok(result) => result,
        };
    
        let directions: Vec<_> = input[0].chars().collect();
    
        let nodes: HashMap<String, Node> = input[2..]
            .iter()
            .map(|l| {
                let (n,l,r) = node_values(&l);
                (n.clone(), Node { name: n, left: l, right: r })
            })
            .collect();
    
        // Part 1
        let mut steps = 0;
        let mut current = "AAA";
        loop {
            let dir = directions[steps % directions.len()];
            steps += 1;
            let next = match dir {
                'L' => &nodes.get(current).unwrap().left,
                'R' => &nodes.get(current).unwrap().right,
                _ => panic!("Unknown direction: {dir}"),
            };
            current = &next;
            if current == "ZZZ" { break }
        }
        println!("Part 1: {steps}"); // 19951
    
        // Part 2
        let mut steps = 0;
        let mut ghosts = nodes.keys().filter(|k| k.ends_with("A")).collect::<Vec<_>>();
        let mut z_steps = vec![0; ghosts.len()];
        loop {
            let dir = directions[steps % directions.len()];
            steps += 1;
            let next_ghosts: Vec<_> = ghosts
                .clone()
                .iter()
                .enumerate()
                .map(|(i,g)| {
                    let next = match dir {
                        'L' => &nodes.get(*g).unwrap().left,
                        'R' => &nodes.get(*g).unwrap().right,
                        _ => panic!("Unknown direction: {dir}"),
                    };
                    if next.ends_with("Z") { z_steps[i] = steps }
                    next
                })
                .collect();
            ghosts = next_ghosts.clone();
    
            if ghosts.iter().all(|g| g.ends_with("Z")) { break } // Normal exit
            if z_steps.iter().all(|z| z != &0usize) { break }    // Optimized exit
        }
        println!("Part 2: {}",lcm(&z_steps)); // 16342438708751
    
        Ok(())
    }
    
    fn lcm(numbers: &Vec<usize>) -> usize {
        numbers.iter().fold(1, |acc, &x| num::integer::lcm(acc,x))
    }
    
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    1 vote
  13. spit-evil-olive-tips
    Link
    part 1 package day08 import ( "fmt" "strings" "github.com/dlclark/regexp2" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "spit-evil-olive.tips/aoc2023/common" ) var nodeRegex =...
    part 1
    package day08
    
    import (
        "fmt"
        "strings"
    
        "github.com/dlclark/regexp2"
        "github.com/rs/zerolog/log"
        "github.com/spf13/cobra"
    
        "spit-evil-olive.tips/aoc2023/common"
    )
    
    var nodeRegex = regexp2.MustCompile(`(?<location>\w+) = \((?<left>\w+), (?<right>\w+)\)`, regexp2.None)
    
    type Node struct {
        location string
        left     string
        right    string
    }
    
    type Map map[string]Node
    
    func parseInput(path string) (string, Map, error) {
        var directions string
        var m Map = make(Map)
    
        parts, err := common.ReadFileParts(path, "\n\n")
        if err != nil {
            return directions, m, err
        }
        directions = parts[0]
    
        for _, line := range strings.Split(parts[1], "\n") {
            match, err := nodeRegex.FindStringMatch(line)
            if err != nil {
                return directions, m, err
            }
    
            if match == nil {
                return directions, nil, fmt.Errorf("failed to parse %v", line)
            }
    
            location := match.GroupByName("location").String()
            node := Node{
                location,
                match.GroupByName("left").String(),
                match.GroupByName("right").String(),
            }
            m[location] = node
        }
    
        return directions, m, nil
    }
    
    func (m Map) Walk(start string, target string, directions string) int {
        steps := 0
    
        current := start
        for {
            if current == target {
                return steps
            }
    
            direction := directions[steps%len(directions)]
    
            var next string
            switch direction {
            case 'L':
                next = m[current].left
            case 'R':
                next = m[current].right
            }
    
            steps++
            current = next
        }
    }
    
    var CommandA = &cobra.Command{
        Use:  "08a",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            directions, m, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            steps := m.Walk("AAA", "ZZZ", directions)
            log.Info().Msgf("steps: %v", steps)
    
            return nil
        },
    }
    
    part 2
    func (m Map) walkGhosts(directions string) int {
        var ghosts []string
        for location, _ := range m {
            if location[2] == 'A' {
                ghosts = append(ghosts, location)
            }
        }
    
        var lengths []int
        for _, start := range ghosts {
            length := m.PathLength(start, directions)
            lengths = append(lengths, length)
        }
    
        total := common.LCM(lengths)
    
        return total
    }
    
    func (m Map) PathLength(start string, directions string) int {
        steps := 0
    
        current := start
        for {
            if current[2] == 'Z' {
                return steps
            }
    
            direction := directions[steps%len(directions)]
    
            var next string
            switch direction {
            case 'L':
                next = m[current].left
            case 'R':
                next = m[current].right
            }
    
            steps++
            current = next
        }
    }
    
    var CommandB = &cobra.Command{
        Use:  "08b",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            directions, m, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            steps := m.walkGhosts(directions)
            log.Info().Msgf("steps: %v", steps)
    
            return nil
        },
    }
    
    addition to my common package
    package common
    
    func GCD(x, y int) int {
        for y != 0 {
            t := y
            y = x % y
            x = t
        }
        return x
    }
    
    func LCM(n []int) int {
        var lcm int
    
        x, y := n[0], n[1]
        lcm = x * y / GCD(x, y)
    
        for i := 2; i < len(n); i++ {
            lcm = lcm * n[i] / GCD(lcm, n[i])
        }
    
        return lcm
    }
    
    performance
    > env BENCHMARK=100 go run main.go 08a day08/08.txt
    {"level":"info","time":"2023-12-09T20:30:53.587086939-08:00","message":"mean: 1.582547ms"}
    {"level":"info","time":"2023-12-09T20:30:53.587091026-08:00","message":"stddev: 210.491µs"}
    > env BENCHMARK=100 go run main.go 08b day08/08.txt
    {"level":"info","time":"2023-12-09T20:31:03.433171993-08:00","message":"mean: 2.976401ms"}
    {"level":"info","time":"2023-12-09T20:31:03.433174969-08:00","message":"stddev: 255.6µs"}
    
    1 vote
  14. Toric
    Link
    im not super happy with part 2, as to solve it I had to make some assumptions that were true in my input (and apparently everyone elses input) but not stated in the problem....

    im not super happy with part 2, as to solve it I had to make some assumptions that were true in my input (and apparently everyone elses input) but not stated in the problem.
    https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day08

    1 vote
  15. jzimbel
    Link
    Elixir As many others have pointed out, part 2 wasn't very fun because I initially considered that each "ghost" might not always start on step 0 of its respective loop, thus making it far more...

    Elixir

    As many others have pointed out, part 2 wasn't very fun because I initially considered that each "ghost" might not always start on step 0 of its respective loop, thus making it far more difficult to calculate where they match up. It wasn't so bad once I figured out that they all start on step 0 and don't have any "sub-loops" / pass through multiple __Z labels, but those properties weren't made clear by the puzzle description.

    New lil' Math module

    There have been a handful of puzzles that involve calculating GCD/LCM, and I always forget how, so I figured I'd create a reusable module for those and other slightly-more-than-basic math functions.

    defmodule AdventOfCode.Math do
      @moduledoc """
      Common math functions/algorithms.
      """
    
      @doc """
      Greatest common denominator.
      """
      @spec gcd(Enumerable.t(pos_integer)) :: pos_integer
      def gcd(ns), do: Enum.reduce(ns, &gcd/2)
    
      defp gcd(d, 0), do: d
      defp gcd(a, b), do: gcd(b, Integer.mod(a, b))
    
      @doc """
      Least common multiple.
      """
      @spec lcm(Enumerable.t(pos_integer)) :: pos_integer
      def lcm(ns), do: Enum.reduce(ns, &lcm/2)
    
      defp lcm(a, b), do: div(a * b, gcd(a, b))
    end
    
    Parts 1 and 2

    I've been stubbornly avoiding regexes for parsing inputs this year, because binary pattern matching is neat and weird. As long as the values you're trying to pull out of the string have a constant character length (variable length allowed if it's at the end of the string), binary pattern matching is sufficient.

    defmodule AdventOfCode.Solution.Year2023.Day08 do
      alias AdventOfCode.Math
    
      @type t :: %__MODULE__{
              labels: %{(index :: non_neg_integer) => String.t()},
              step: non_neg_integer,
              zs: MapSet.t(step :: non_neg_integer)
            }
      @enforce_keys [:labels]
      defstruct @enforce_keys ++ [step: 0, zs: MapSet.new()]
    
      def new(labels) do
        labels
        |> Enum.with_index(fn l, i -> {i, l} end)
        |> Map.new()
        |> then(&%__MODULE__{labels: &1})
      end
    
      def part1(input) do
        input
        |> parse()
        |> steps_to_z(["AAA"], &(&1 == "ZZZ"))
      end
    
      def part2(input) do
        input
        |> parse()
        |> steps_to_z(&match?(<<_, _, ?Z>>, &1))
      end
    
      defp steps_to_z({dirs, map}, done?) do
        steps_to_z({dirs, map}, for(<<_, _, ?A>> = label <- Map.keys(map), do: label), done?)
      end
    
      defp steps_to_z({dirs, map}, starting_labels, done?) do
        dirs
        |> Stream.cycle()
        |> Enum.reduce_while(new(starting_labels), fn dir, acc ->
          if map_size(acc.labels) == 0 do
            {:halt, acc.zs}
          else
            {:cont, update_acc(acc, map, dir, done?)}
          end
        end)
        |> Math.lcm()
      end
    
      defp update_acc(acc, map, dir, done?) do
        found_z = Enum.flat_map(acc.labels, fn {i, l} -> if done?.(l), do: [i], else: [] end)
    
        labels =
          acc.labels
          |> Map.drop(found_z)
          |> Map.new(fn {i, l} -> {i, map[l][dir]} end)
    
        zs = if match?([_ | _], found_z), do: MapSet.put(acc.zs, acc.step), else: acc.zs
    
        %{acc | labels: labels, zs: zs, step: acc.step + 1}
      end
    
      defp parse(input) do
        [dirs, map] = String.split(input, "\n\n")
    
        dirs = for <<dir::1-bytes <- dirs>>, do: dir |> String.downcase() |> String.to_existing_atom()
    
        map =
          for <<label::3-bytes, _::4*8, left::3-bytes, _::2*8, right::3-bytes, _::bytes>> <-
                String.split(map, "\n", trim: true),
              into: %{},
              do: {label, %{l: left, r: right}}
    
        {dirs, map}
      end
    end
    
    1 vote