Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 19ffef3

Browse files
committedDec 19, 2023
Simplify logic by converting each rule into a range
1 parent 6229fbf commit 19ffef3

File tree

2 files changed

+73
-94
lines changed

2 files changed

+73
-94
lines changed
 

‎README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ pie
9999
| 16 | [The Floor Will Be Lava](https://adventofcode.com/2023/day/16) | [Source](src/year2023/day16.rs) | 831 |
100100
| 17 | [Clumsy Crucible](https://adventofcode.com/2023/day/17) | [Source](src/year2023/day17.rs) | 2566 |
101101
| 18 | [Lavaduct Lagoon](https://adventofcode.com/2023/day/18) | [Source](src/year2023/day18.rs) | 19 |
102-
| 19 | [Aplenty](https://adventofcode.com/2023/day/19) | [Source](src/year2023/day19.rs) | 107 |
102+
| 19 | [Aplenty](https://adventofcode.com/2023/day/19) | [Source](src/year2023/day19.rs) | 100 |
103103

104104
## 2022
105105

‎src/year2023/day19.rs

Lines changed: 72 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
1+
//! # Aplenty
2+
//!
3+
//! Each rule is converted into a half open interval, including the start but excluding the end.
4+
//! For example:
5+
//!
6+
//! * `x > 10` => `10..4001`
7+
//! * `m < 20` => `1..20`
8+
//! * `A` => `1..4001`
9+
//!
10+
//! For part one if a category is contained in a range, we send the part to the next rule,
11+
//! stopping when `A` or `R` is reached.
12+
//!
13+
//! For part two we perform range splitting similar to [`Day 5`] that converts the category into
14+
//! 1, 2 or 3 new ranges, then sends those ranges to the respective rule.
15+
//!
16+
//! [`Day 5`]: crate::year2023::day05
117
use crate::util::hash::*;
218
use crate::util::iter::*;
319
use crate::util::parse::*;
4-
use Rule::*;
5-
6-
#[derive(Clone, Copy, Debug)]
7-
enum Rule<'a> {
8-
Send(&'a str),
9-
Less(usize, u32, &'a str),
10-
Greater(usize, u32, &'a str),
11-
Equal(usize, u32, &'a str),
20+
21+
pub struct Rule<'a> {
22+
start: u32,
23+
end: u32,
24+
category: usize,
25+
next: &'a str,
1226
}
1327

1428
pub struct Input<'a> {
1529
workflows: FastMap<&'a str, Vec<Rule<'a>>>,
1630
parts: &'a str,
1731
}
1832

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

2845
for [first, second] in iter.chunk::<2>() {
2946
let rule = if second.is_empty() {
30-
Send(first)
47+
// The last rule will match everything so pick category 0 arbitrarily.
48+
Rule { start: 1, end: 4001, category: 0, next: first }
3149
} else {
50+
// Map each category to an index for convenience so that we can store a part
51+
// in a fixed size array.
3252
let category = match first.as_bytes()[0] {
3353
b'x' => 0,
3454
b'm' => 1,
@@ -38,11 +58,12 @@ pub fn parse(input: &str) -> Input<'_> {
3858
};
3959

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

63+
// Convert each rule into a half open range.
4264
match first.as_bytes()[1] {
43-
b'<' => Less(category, value, second),
44-
b'>' => Greater(category, value, second),
45-
b'=' => Equal(category, value, second),
65+
b'<' => Rule { start: 1, end: value, category, next },
66+
b'>' => Rule { start: value + 1, end: 4001, category, next },
4667
_ => unreachable!(),
4768
}
4869
};
@@ -60,42 +81,23 @@ pub fn part1(input: &Input<'_>) -> u32 {
6081
let Input { workflows, parts } = input;
6182
let mut result = 0;
6283

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

66-
loop {
67-
if key == "A" {
68-
result += part.iter().sum::<u32>();
69-
break;
70-
}
71-
if key == "R" {
72-
break;
73-
}
74-
75-
for &rule in &workflows[key] {
76-
match rule {
77-
Send(next) => key = next,
78-
Less(category, value, next) => {
79-
if part[category] < value {
80-
key = next;
81-
break;
82-
}
83-
}
84-
Greater(category, value, next) => {
85-
if part[category] > value {
86-
key = next;
87-
break;
88-
}
89-
}
90-
Equal(category, value, next) => {
91-
if part[category] == value {
92-
key = next;
93-
break;
94-
}
95-
}
88+
while key.len() > 1 {
89+
// Find the first matching rule.
90+
for &Rule { start, end, category, next } in &workflows[key] {
91+
if start <= part[category] && part[category] < end {
92+
key = next;
93+
break;
9694
}
9795
}
9896
}
97+
98+
if key == "A" {
99+
result += part.iter().sum::<u32>();
100+
}
99101
}
100102

101103
result
@@ -104,64 +106,41 @@ pub fn part1(input: &Input<'_>) -> u32 {
104106
pub fn part2(input: &Input<'_>) -> u64 {
105107
let Input { workflows, .. } = input;
106108
let mut result = 0;
107-
let mut todo = vec![("in", 0, [(1, 4000); 4])];
109+
let mut todo = vec![("in", 0, [(1, 4001); 4])];
108110

109111
while let Some((key, index, mut part)) = todo.pop() {
110-
if key == "A" {
111-
result += part.iter().map(|(s, e)| (e - s + 1) as u64).product::<u64>();
112-
continue;
113-
}
114-
if key == "R" {
112+
if key.len() == 1 {
113+
if key == "A" {
114+
result += part.iter().map(|(s, e)| (e - s) as u64).product::<u64>();
115+
}
115116
continue;
116117
}
117118

118-
match workflows[key][index] {
119-
Send(next) => todo.push((next, 0, part)),
120-
Less(category, value, next) => {
121-
let (start, end) = part[category];
122-
123-
if start >= value {
124-
todo.push((key, index + 1, part));
125-
} else if end < value {
126-
todo.push((next, 0, part));
127-
} else {
128-
part[category] = (start, value - 1);
129-
todo.push((next, 0, part));
130-
131-
part[category] = (value, end);
132-
todo.push((key, index + 1, part));
133-
}
119+
let Rule { start: s2, end: e2, category, next } = workflows[key][index];
120+
let (s1, e1) = part[category];
121+
122+
// x1 and x2 are the possible overlap.
123+
let x1 = s1.max(s2);
124+
let x2 = e1.min(e2);
125+
126+
if x1 >= x2 {
127+
// No overlap. Check the next rating.
128+
todo.push((key, index + 1, part));
129+
} else {
130+
// Range that overlaps with the rating.
131+
part[category] = (x1, x2);
132+
todo.push((next, 0, part));
133+
134+
// Range before rating.
135+
if s1 < x1 {
136+
part[category] = (s1, x1);
137+
todo.push((key, index + 1, part));
134138
}
135-
Greater(category, value, next) => {
136-
let (start, end) = part[category];
137-
138-
if end <= value {
139-
todo.push((key, index + 1, part));
140-
} else if start > value {
141-
todo.push((next, 0, part));
142-
} else {
143-
part[category] = (start, value);
144-
todo.push((key, index + 1, part));
145-
146-
part[category] = (value + 1, end);
147-
todo.push((next, 0, part));
148-
}
149-
}
150-
Equal(category, value, next) => {
151-
let (start, end) = part[category];
152-
153-
if start > value || end < value {
154-
todo.push((key, index + 1, part));
155-
} else {
156-
part[category] = (start, value - 1);
157-
todo.push((key, index + 1, part));
158139

159-
part[category] = (value, value);
160-
todo.push((next, 0, part));
161-
162-
part[category] = (value + 1, end);
163-
todo.push((key, index + 1, part));
164-
}
140+
// Range after rating.
141+
if x2 < e1 {
142+
part[category] = (x2, e1);
143+
todo.push((key, index + 1, part));
165144
}
166145
}
167146
}

0 commit comments

Comments
 (0)
Please sign in to comment.