mike's website

embedded, robots, rust

<<<

AoC '23 4A

Problem Statement

Each pair of numbers in the list indicates a range, so 6-8 indicates 6, 7, and 8. There are two pairs per list item. We want to distinguish pairs like 5-7,4-8 – in those cases, 5-7 entirely overlaps 4-8, from a pair like 5-7 and 6-8, for which 5 and 8 do not overlap. Our end result should be a final count of every completely overlapping set.

Here is the example puzzle input:

2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8

Approach

So here, there are a couple of ways to go: I could use HashSet again, and check if one set is a subset of another. So, assuming we have A and B, we can check: (A is a subset of B OR B is a subset of A). This would filter matches properly.

However, it might be inefficient to create two HashSets for every comparison. While using HashSets would be useful as a general strategy, our problem has a more specific structure: the elements are already sorted; we know the upper and lower bounds for each pair! This extra structure allows us to do what we need only with some clever arithmetic.

We’ll start by noticing that we can easily obtain the size of the two sets by subtracting the upper element from the lower element of each. If we say that set \(A\) is the larger set and set \(B\) is the smaller set, then we can determine the necessary and sufficient conditions for a valid pair. We’ll also say that each has an upper and lower bound, \(u\) and \(l\). So \(u_A\) is the upper bound of A, and so on.

Let’s establish a proposition. Given that \(A \) is larger (has more elements) than \(B\):

$$ u_A \geq u_B \land l_A \leq l_B \implies B \subseteq A $$

To prove this:

With this proof in our pocket, we are ready to devise an algorithm:

def part_one(input):
    count = 0
    for line in input:
        # get elements
        l1, l2 = line[0], line[2]
        l3, l4 = line[4], line[6]
        # find which set has more elements
        if l2-l1 >= l4-l3:
            u_a, l_a = l2, l1
            u_b, l_b = l4, l3
        else:
            u_a, l_a = l4, l3
            u_b, l_b = l2, l1
        # filter
        if l_a <= l_b and u_a >= u_b:
            count += 1
        else:
            continue
    return count

Since we’re only doing some basic arithmetic for each line, this algorithm runs in \(O(n)\) time. Assuming we’ve done our proof correctly, this will get us the count that we need!

Solution

Unlike extracting characters, like we did in the rock-paper-scissors game before, we actually need to match digits, because digits can be 1+ characters: like 5, or 50, or 500. For this, then, we use the regex crate. I really like Rust’s packaging system. All it took was cargo add regex and we’re off to the races.

Now, regex compiles its patterns when the program is run. Here’s the code to extract the digits from a line of text from our puzzle input (remember that a line looks like 3-15,6-10):

use lazy_static::lazy_static;
use regex::Regex;

fn get_digits(text: &str) -> (u32, u32, u32, u32) {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"(\d+)").unwrap();
    }
    let mut matches = RE.find_iter(text);
    let a: u32 = matches.next().unwrap().as_str().parse().unwrap();
    let b: u32 = matches.next().unwrap().as_str().parse().unwrap();
    let c: u32 = matches.next().unwrap().as_str().parse().unwrap();
    let d: u32 = matches.next().unwrap().as_str().parse().unwrap();
    return (a, b, c, d)
}

We use the lazy_static crate to ensure that the compilation that occurs on line 6 only occurs once – even though Regex::new() is called many times in a loop when our program reads each line from the input.

We can trust that the input will have exactly 4 digits and no more, so we can use .unwrap() to shorten the code. But in a general case – like if we were parsing text, we would need to catch all of the potential degenerate lines. What if one of our inputs was 4-5,mike-10?

The rest follows from the proof we sketched above. We again use the functional notation to just sum everything into res.

pub fn part_one(input: &str) -> Option<u32> {
    let res:u32 = input.lines().map(get_digits).map(|d|{
        let l_a:u32;
        let l_b:u32;
        let u_a:u32;
        let u_b:u32;
        if d.1 - d.0 > d.3 - d.2 {
            l_a = d.0;
            u_a = d.1;
            l_b = d.2;
            u_b = d.3;
        }
        else {
            l_a = d.2;
            u_a = d.3;
            l_b = d.0;
            u_b = d.1;
        }
        if l_a <= l_b && u_a >= u_b {
            return 1;
        }
        else {
            return 0;
        }
    }).sum();
    return Some(res)
}

Onto the next!