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 width = *width;
61 let mut visible = vec![false; digits.len()];
62
63 for i in 1..(width - 1) {
64 let mut left_max = -1;
65 let mut right_max = -1;
66 let mut top_max = -1;
67 let mut bottom_max = -1;
68
69 for j in 0..(width - 1) {
70 let left = (i * width) + j;
71 if digits[left] > left_max {
72 visible[left] = true;
73 left_max = digits[left];
74 }
75
76 let right = (i * width) + (width - j - 1);
77 if digits[right] > right_max {
78 visible[right] = true;
79 right_max = digits[right];
80 }
81
82 let top = (j * width) + i;
83 if digits[top] > top_max {
84 visible[top] = true;
85 top_max = digits[top];
86 }
87
88 let bottom = (width - j - 1) * width + i;
89 if digits[bottom] > bottom_max {
90 visible[bottom] = true;
91 bottom_max = digits[bottom];
92 }
93 }
94 }
95
96 4 + visible.iter().filter(|&&b| b).count()
97}
98
99/// Calculate the distance visible in each direction by using 10 rolling maximum values for each
100/// height packed into a single `u64`.
101///
102/// Part 2 is similar to part 1, but instead of keeping a single maximum for each direction, we
103/// need to keep an *array* of 10 values, one for each possible height.
104///
105/// For each tree its score is the current value at the same index as its height. Then we increment
106/// the value of previously seen trees greater than the current height by one
107/// and reset the values of trees less than or equal than the current height to one.
108///
109/// We skip processing the edge and corner trees. Strictly speaking their score should be zero, but
110/// as the maximum value will always be greater than or equal to one, it's fine to leave them
111/// bulk initialized to one.
112///
113/// Using the fourth row of the sample and going left to right:
114///
115/// | Tree | Scenic Array | Score |
116/// |---|---|---|
117/// | 3 | [1, 1, 1, (1), 1, 1, 1, 1, 1, 1] | 1
118/// | 5 | [1, 1, 1, 1, 2, (2), 2, 2, 2, 2] | 2
119/// | 4 | [1, 1, 1, 1, (1), 1, 3, 3, 3, 3] | 1
120///
121/// Instead of using an array and iterating over it to update the values, we can achieve the same
122/// result much faster by packing the ten values into a single `u64` in blocks of 6 bits. 6 bits
123/// gives a maximum value of `2^6 = 64` that's a bit of a gamble. It's less than the maximum
124/// possible value of 98 that could be theoretically encountered but should work for most inputs.
125///
126/// To obtain the current value we right shift by the current height times 6 (this is why we
127/// multiplied by 6 in the [`parse`] function) and mask only the least significant 6 bits.
128///
129/// To update the next value, we first use [`MASK`] left shifted to zero out all bits less than
130/// or equal to the current height then add 1 to all values in parallel using the [`ONES`] pattern.
131///
132/// For example going from 3 to 5 in the sample above:
133/// ```none
134/// scenic: 000001_000001_000001_000001_000001_000001_000001_000001_000001_000001
135/// mask: 111111_111111_111111_111111_111111_111111_000000_000000_000000_000000
136/// scenic & mask: 000001_000001_000001_000001_000001_000001_000000_000000_000000_000000
137/// scenic + ones: 000010_000010_000010_000010_000010_000010_000001_000001_000001_000001
138/// ```
139pub fn part2(input: &Input) -> u64 {
140 let (width, digits) = input;
141 let width = *width;
142 let mut scenic = vec![1; digits.len()];
143
144 for i in 1..(width - 1) {
145 let mut left_max = ONES;
146 let mut right_max = ONES;
147 let mut top_max = ONES;
148 let mut bottom_max = ONES;
149
150 for j in 1..(width - 1) {
151 let left = (i * width) + j;
152 scenic[left] *= (left_max >> digits[left]) & 0x3f;
153 left_max = (left_max & (MASK << digits[left])) + ONES;
154
155 let right = (i * width) + (width - j - 1);
156 scenic[right] *= (right_max >> digits[right]) & 0x3f;
157 right_max = (right_max & (MASK << digits[right])) + ONES;
158
159 let top = (j * width) + i;
160 scenic[top] *= (top_max >> digits[top]) & 0x3f;
161 top_max = (top_max & (MASK << digits[top])) + ONES;
162
163 let bottom = (width - j - 1) * width + i;
164 scenic[bottom] *= (bottom_max >> digits[bottom]) & 0x3f;
165 bottom_max = (bottom_max & (MASK << digits[bottom])) + ONES;
166 }
167 }
168
169 *scenic.iter().max().unwrap()
170}