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}