aoc/year2022/day08.rs
1//! # Treetop Tree House
2//!
3//! Part one is solved with an efficient `O(n)` algorithm. Part two is also solved with an efficient `O(n)`
4//! algorithm, using a bit manipulation trick to make the complexity independent of the number of digits.
5const ONES: u64 = 0x0041041041041041;
6const MASK: u64 = 0x0fffffffffffffc0;
7
8type Input = (usize, Vec<i8>);
9
10/// Convert a 2D grid of ASCII digits into a 1D `vec` of heights.
11///
12/// Each height is multiplied by 6. For part one this makes no difference, but for part two this helps
13/// with the bit manipulation.
14///
15/// To convert from 2D coordinates to an index, the formula is `y * width + x`. For the sample grid
16/// of width 5, the top middle point at `(2, 0)` is at index `0 * 5 + 2 = 2` and the point directly
17/// below `(2, 1)` is at index `1 * 5 + 2 = 7`.
18///
19/// Using a 1D `vec` instead of a `vec` of `vec`s is faster for 2 reasons:
20/// * Avoids an intermediate pointer lookup for each access.
21/// * Better cache locality as the memory locations are adjacent and not potentially
22/// scattered all over the heap.
23pub fn parse(input: &str) -> Input {
24 let raw: Vec<_> = input.lines().collect();
25 let width = raw[0].len();
26 let mut digits = Vec::with_capacity(raw.len() * width);
27
28 for line in &raw {
29 digits.extend(line.bytes().map(|b| 6 * (b - b'0') as i8));
30 }
31
32 (width, digits)
33}
34
35/// Calculate visible trees using a rolling maximum for each row and column in left, right, up
36/// and down directions.
37///
38/// Using the top row of the sample and going left to right:
39///
40/// | Tree | Max | Visible |
41/// |---|---|---|
42/// | 3 | -1 | true |
43/// | 0 | 3 | false |
44/// | 3 | 3 | false |
45/// | 7 | 3 | true |
46///
47/// The last tree in each row and column doesn't need to be checked since it's covered
48/// by the loop in the opposite direction.
49///
50/// A tree is visible if it can be seen from any direction. As a minor optimization, rather
51/// than have 4 separate loop pairs, the left, right, up and down loops are all rolled into
52/// one pair, to amortise the cost of loop logic.
53///
54/// The 4 corner trees don't need to be checked since they're always visible
55/// so they're added directly to the total.
56pub fn part1(input: &Input) -> usize {
57 let (width, digits) = input;
58 let width = *width;
59 let mut visible = vec![false; digits.len()];
60
61 for i in 1..(width - 1) {
62 let mut left_max = -1;
63 let mut right_max = -1;
64 let mut top_max = -1;
65 let mut bottom_max = -1;
66
67 for j in 0..(width - 1) {
68 let left = (i * width) + j;
69 if digits[left] > left_max {
70 visible[left] = true;
71 left_max = digits[left];
72 }
73
74 let right = (i * width) + (width - j - 1);
75 if digits[right] > right_max {
76 visible[right] = true;
77 right_max = digits[right];
78 }
79
80 let top = (j * width) + i;
81 if digits[top] > top_max {
82 visible[top] = true;
83 top_max = digits[top];
84 }
85
86 let bottom = (width - j - 1) * width + i;
87 if digits[bottom] > bottom_max {
88 visible[bottom] = true;
89 bottom_max = digits[bottom];
90 }
91 }
92 }
93
94 4 + visible.iter().filter(|&&b| b).count()
95}
96
97/// Calculate the distance visible in each direction by using 10 rolling maximum values for each
98/// height packed into a single `u64`.
99///
100/// Part two is similar to part one, but instead of keeping a single maximum for each direction, we
101/// need to keep an *array* of 10 values, one for each possible height.
102///
103/// For each tree its score is the current value at the same index as its height. Then we increment
104/// the value of previously seen trees greater than the current height by one
105/// and reset the values of trees less than or equal to the current height to one.
106///
107/// We skip processing the edge and corner trees. Strictly speaking their score should be zero, but
108/// as the maximum value will always be greater than or equal to one, it's fine to leave them
109/// bulk initialized to one.
110///
111/// Using the fourth row of the sample and going left to right:
112///
113/// | Tree | Scenic Array | Score |
114/// |---|---|---|
115/// | 3 | [1, 1, 1, (1), 1, 1, 1, 1, 1, 1] | 1
116/// | 5 | [1, 1, 1, 1, 2, (2), 2, 2, 2, 2] | 2
117/// | 4 | [1, 1, 1, 1, (1), 1, 3, 3, 3, 3] | 1
118///
119/// Instead of using an array and iterating over it to update the values, we can achieve the same
120/// result much faster by packing the ten values into a single `u64` in blocks of 6 bits. 6 bits
121/// gives a maximum value of `2⁶ = 64` that's a bit of a gamble. It's less than the maximum
122/// possible value of 98 that could be theoretically encountered but should work for most inputs.
123///
124/// To obtain the current value we right shift by the current height times 6 (this is why we
125/// multiplied by 6 in the [`parse`] function) and mask only the least significant 6 bits.
126///
127/// To update the next value, we first use [`MASK`] left shifted to zero out all bits less than
128/// or equal to the current height then add 1 to all values in parallel using the [`ONES`] pattern.
129///
130/// For example, going from 3 to 5 in the sample above:
131/// ```none
132/// scenic: 000001_000001_000001_000001_000001_000001_000001_000001_000001_000001
133/// mask: 111111_111111_111111_111111_111111_111111_000000_000000_000000_000000
134/// scenic & mask: 000001_000001_000001_000001_000001_000001_000000_000000_000000_000000
135/// scenic + ones: 000010_000010_000010_000010_000010_000010_000001_000001_000001_000001
136/// ```
137pub fn part2(input: &Input) -> u64 {
138 let (width, digits) = input;
139 let width = *width;
140 let mut scenic = vec![1; digits.len()];
141
142 for i in 1..(width - 1) {
143 let mut left_max = ONES;
144 let mut right_max = ONES;
145 let mut top_max = ONES;
146 let mut bottom_max = ONES;
147
148 for j in 1..(width - 1) {
149 let left = (i * width) + j;
150 scenic[left] *= (left_max >> digits[left]) & 0x3f;
151 left_max = (left_max & (MASK << digits[left])) + ONES;
152
153 let right = (i * width) + (width - j - 1);
154 scenic[right] *= (right_max >> digits[right]) & 0x3f;
155 right_max = (right_max & (MASK << digits[right])) + ONES;
156
157 let top = (j * width) + i;
158 scenic[top] *= (top_max >> digits[top]) & 0x3f;
159 top_max = (top_max & (MASK << digits[top])) + ONES;
160
161 let bottom = (width - j - 1) * width + i;
162 scenic[bottom] *= (bottom_max >> digits[bottom]) & 0x3f;
163 bottom_max = (bottom_max & (MASK << digits[bottom])) + ONES;
164 }
165 }
166
167 *scenic.iter().max().unwrap()
168}