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