Skip to content

Commit 0125085

Browse files
committed
Compute result byte-by-byte without splitting string
This puzzle is very amenable to processing one byte at a time, with our own state machine. This is lower overhead than letting split find the boundary between passengers only to then revisit the bytes of the passenger ourselves. Masking `1 << (ch & 31)` maps b'a' to bit 1, but is more efficient than the more obvious `1 << (ch - b'a')` which maps b'a' to bit 0, because on x86 hardware, the compiler can perform the mask implicitly as part of the bit-shift instruction. The test suite reflects that actual input includes the trailing newline, because the code now depends on being able to append a second newline to not have to special-case the last group. On my laptop, this cuts runtime from 73us (parse at 63us, part1 and part2 at 5 use each) to just 18us (all in parse).
1 parent 7797777 commit 0125085

2 files changed

Lines changed: 49 additions & 11 deletions

File tree

src/year2020/day06.rs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,59 @@
1010
//! [bitwise OR](https://en.wikipedia.org/wiki/Bitwise_operation) then count the number of ones
1111
//! for each group using the blazing fast [`count_ones`] intrinsic.
1212
//!
13-
//! Part two is very similar, except that we use a bitwise AND instead.
13+
//! Part two is very similar, except that we use a bitwise AND instead. It is faster to run
14+
//! both parts at once during the parse.
1415
//!
1516
//! [`day 5`]: crate::year2020::day05
1617
//! [`count_ones`]: u32::count_ones
17-
pub fn parse(input: &str) -> Vec<u32> {
18-
input.lines().map(|line| line.bytes().fold(0, |acc, b| acc | (1 << (b - b'a')))).collect()
19-
}
18+
use std::iter::once;
19+
20+
type Input = (u32, u32);
21+
22+
pub fn parse(input: &str) -> Input {
23+
let mut any = 0_u32;
24+
let mut all = u32::MAX;
25+
let mut passenger = 0;
26+
let mut part_one = 0;
27+
let mut part_two = 0;
28+
let iter = input.bytes();
29+
let mut check_group = false;
30+
31+
// End the input with a double newline, to include last group in the total.
32+
for ch in iter.chain(once(b'\n')) {
33+
match ch {
34+
b'a'..=b'z' => {
35+
// A letter sets the next bit for the given passenger.
36+
// Masking to bits 1-26 is more efficient than subtracting to bits 0-25.
37+
check_group = false;
38+
passenger |= 1 << (ch & 31);
39+
}
40+
b'\n' if check_group => {
41+
// A second newline ends one group and prepares for the next.
42+
check_group = false;
43+
part_one += any.count_ones();
44+
part_two += all.count_ones();
45+
any = 0;
46+
all = u32::MAX;
47+
}
48+
b'\n' => {
49+
// A single newline separates passengers within a group.
50+
any |= passenger;
51+
all &= passenger;
52+
passenger = 0;
53+
check_group = true;
54+
}
55+
_ => unreachable!(),
56+
}
57+
}
2058

21-
pub fn part1(input: &[u32]) -> u32 {
22-
count_answers(input, u32::MIN, |group, passenger| group | passenger)
59+
(part_one, part_two)
2360
}
2461

25-
pub fn part2(input: &[u32]) -> u32 {
26-
count_answers(input, u32::MAX, |group, passenger| group & passenger)
62+
pub fn part1(input: &Input) -> u32 {
63+
input.0
2764
}
2865

29-
fn count_answers(input: &[u32], initial: u32, combine: fn(u32, &u32) -> u32) -> u32 {
30-
input.split(|&p| p == 0).map(|group| group.iter().fold(initial, combine).count_ones()).sum()
66+
pub fn part2(input: &Input) -> u32 {
67+
input.1
3168
}

tests/year2020/day06.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ a
1515
a
1616
a
1717
18-
b";
18+
b
19+
";
1920

2021
#[test]
2122
fn part1_test() {

0 commit comments

Comments
 (0)