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}