1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! # Aplenty
//!
//! Each rule is converted into a half open interval, including the start but excluding the end.
//! For example:
//!
//! * `x > 10` => `10..4001`
//! * `m < 20` => `1..20`
//! * `A` => `1..4001`
//!
//! For part one if a category is contained in a range, we send the part to the next rule,
//! stopping when `A` or `R` is reached.
//!
//! For part two we perform range splitting similar to [`Day 5`] that converts the category into
//! 1, 2 or 3 new ranges, then sends those ranges to the respective rule.
//!
//! [`Day 5`]: crate::year2023::day05
use crate::util::hash::*;
use crate::util::iter::*;
use crate::util::parse::*;

pub struct Rule<'a> {
    start: u32,
    end: u32,
    category: usize,
    next: &'a str,
}

pub struct Input<'a> {
    workflows: FastMap<&'a str, Vec<Rule<'a>>>,
    parts: &'a str,
}

/// Parse each rule from the first half of the input.
/// Leaves the second half of the input as a `&str` as it's faster to iterate over each chunk of
/// four numbers than to first collect into a `vec`.
pub fn parse(input: &str) -> Input<'_> {
    let (prefix, suffix) = input.split_once("\n\n").unwrap();
    let mut workflows = FastMap::with_capacity(1000);

    for line in prefix.lines() {
        let mut rules = Vec::with_capacity(5);
        let mut iter = line.split(['{', ':', ',', '}']);
        let key = iter.next().unwrap();

        for [first, second] in iter.chunk::<2>() {
            let rule = if second.is_empty() {
                // The last rule will match everything so pick category 0 arbitrarily.
                Rule { start: 1, end: 4001, category: 0, next: first }
            } else {
                // Map each category to an index for convenience so that we can store a part
                // in a fixed size array.
                let category = match first.as_bytes()[0] {
                    b'x' => 0,
                    b'm' => 1,
                    b'a' => 2,
                    b's' => 3,
                    _ => unreachable!(),
                };

                let value: u32 = (&first[2..]).unsigned();
                let next = second;

                // Convert each rule into a half open range.
                match first.as_bytes()[1] {
                    b'<' => Rule { start: 1, end: value, category, next },
                    b'>' => Rule { start: value + 1, end: 4001, category, next },
                    _ => unreachable!(),
                }
            };

            rules.push(rule);
        }

        workflows.insert(key, rules);
    }

    Input { workflows, parts: suffix }
}

pub fn part1(input: &Input<'_>) -> u32 {
    let Input { workflows, parts } = input;
    let mut result = 0;

    // We only care about the numbers and can ignore all delimeters and whitespace.
    for part in parts.iter_unsigned::<u32>().chunk::<4>() {
        let mut key = "in";

        while key.len() > 1 {
            // Find the first matching rule.
            for &Rule { start, end, category, next } in &workflows[key] {
                if start <= part[category] && part[category] < end {
                    key = next;
                    break;
                }
            }
        }

        if key == "A" {
            result += part.iter().sum::<u32>();
        }
    }

    result
}

pub fn part2(input: &Input<'_>) -> u64 {
    let Input { workflows, .. } = input;
    let mut result = 0;
    let mut todo = vec![("in", 0, [(1, 4001); 4])];

    while let Some((key, index, mut part)) = todo.pop() {
        if key.len() == 1 {
            if key == "A" {
                result += part.iter().map(|(s, e)| (e - s) as u64).product::<u64>();
            }
            continue;
        }

        let Rule { start: s2, end: e2, category, next } = workflows[key][index];
        let (s1, e1) = part[category];

        // x1 and x2 are the possible overlap.
        let x1 = s1.max(s2);
        let x2 = e1.min(e2);

        if x1 >= x2 {
            // No overlap. Check the next rating.
            todo.push((key, index + 1, part));
        } else {
            // Range that overlaps with the rating.
            part[category] = (x1, x2);
            todo.push((next, 0, part));

            // Range before rating.
            if s1 < x1 {
                part[category] = (s1, x1);
                todo.push((key, index + 1, part));
            }

            // Range after rating.
            if x2 < e1 {
                part[category] = (x2, e1);
                todo.push((key, index + 1, part));
            }
        }
    }

    result
}