1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
/* Copyright 2022 Mario Finelli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! Advent of Code 2022 Day 8: <https://adventofcode.com/2022/day/8>
//!
//! Overall this solution is a little bit ugly and a lot of brute force; there
//! are probably better ways to solve this. We start by parsing the input (we
//! do an initial pass `n*n` complexity) to convert the strings in the input
//! into a vector of integer vectors which saves us having to do the parse for
//! every comparison. Then we look at interior trees only. In part one we can
//! do a simple calculation based on the dimensions to account for them
//! without needing to loop through and account for them if x/y==0/len and in
//! part two we can ignore them because as edge trees they have one side with
//! a viewing distance of zero which when multiplied by any other viewing
//! distance will obviously result in a scenic score of zero too. For part two
//! we use the usual [`std::collections::BinaryHeap`] to keep track of the
//! tree with the highest scenic score value.
use std::collections::BinaryHeap;
/// The solution for the day eight challenge.
///
/// Given the input as a string and the part as an integer where `1` returns
/// the number of trees that are visible and `2` returns the highest possible
/// scenic score (viewing distance in all 4 directions multiplied together)
/// we compute the answer for both parts one and two at the same time and then
/// just return the desired answer.
///
/// We start by parsing the input into the vector of integer vectors as
/// described above. We then loop through all of the interior trees one at a
/// time. We look out in each direction (note the range reverse for looking
/// left and up) to determine how many trees we can see until we get a tree
/// that is taller or equal to the tree that we are inspecting (or the edge).
/// If we reach the edge then the tree is visible from that direction and we
/// can increase our visible tree counter. In part one, we could move on to
/// the next tree directly (`continue`) but in part two we need to calculate
/// the viewing distance from all directions. As we look at each other tree
/// we increase the viewing distance for that direction until we get to a
/// view-blocking tree or the edge. As described in the prompt the scenic
/// score is calculated by multiplying the four viewing distances together.
/// After inspecting all four directions we calculate the score and add it to
/// the scenic score heap. If we want the highest scenic score (part `2`) then
/// we pop the heap, otherwise we return the total visible trees.
///
/// # Example
/// ```rust
/// # use aoc::y22d08::y22d08;
/// // probably read this from the input file...
/// let input = "33333\n34223\n32123\n32223\n33333\n";
/// assert_eq!(y22d08(input, 1), 17);
/// assert_eq!(y22d08(input, 2), 9);
/// ```
pub fn y22d08(input: &str, part: u32) -> u32 {
let grid = parse_input(input);
let mut scenic_scores = BinaryHeap::new();
// calculate the outer edge which is always visible
let mut total = 2 * grid.len() as u32 + 2 * (grid[0].len() as u32 - 2);
for y in 1..grid.len() - 1 {
for x in 1..grid[0].len() - 1 {
// consider tree in position x,y: grid[y][x]
let mut visible_from_left = true;
let mut visible_to_left = 0;
for left in (0..x).rev() {
// compare to the left: left,y (grid[y][left])
visible_to_left += 1;
if grid[y][left] >= grid[y][x] {
visible_from_left = false;
break;
}
}
if visible_from_left {
total += 1;
}
let mut visible_from_right = true;
let mut visible_to_right = 0;
for right in x + 1..grid[0].len() {
// compare to the right: right,y (grid[y][right])
visible_to_right += 1;
if grid[y][right] >= grid[y][x] {
visible_from_right = false;
break;
}
}
// don't double count so check previous visibility
if !visible_from_left && visible_from_right {
total += 1;
}
let mut visible_from_top = true;
let mut visible_to_top = 0;
for top in (0..y).rev() {
// compare to the top: x, top (grid[top][x])
visible_to_top += 1;
if grid[top][x] >= grid[y][x] {
visible_from_top = false;
break;
}
}
// don't double count so check previous visibilities
if !visible_from_left && !visible_from_right && visible_from_top {
total += 1;
}
let mut visible_from_bottom = true;
let mut visible_to_bottom = 0;
for bottom in y + 1..grid.len() {
// compare to the bottom: x, bottom (grid[bottom][x])
visible_to_bottom += 1;
if grid[bottom][x] >= grid[y][x] {
visible_from_bottom = false;
break;
}
}
// don't double count so check previous visibilities
if !visible_from_left
&& !visible_from_right
&& !visible_from_top
&& visible_from_bottom
{
total += 1;
}
let scenic_score = visible_to_left
* visible_to_right
* visible_to_top
* visible_to_bottom;
scenic_scores.push(scenic_score);
}
}
if part == 1 {
total
} else {
scenic_scores.pop().unwrap()
}
}
/// Parsing the input requires an extra n^2 time complexity since we need to
/// loop through every element, but I think it should save time overall as
/// otherwise we need to parse the character every time we do a check.
fn parse_input(input: &str) -> Vec<Vec<u32>> {
let mut grid = Vec::new();
let lines: Vec<_> = input.lines().collect();
for line in lines {
let mut trees = Vec::new();
let chars: Vec<_> = line.chars().collect();
for c in chars {
let height: u32 = c.to_string().parse().unwrap();
trees.push(height);
}
grid.push(trees);
}
grid
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn it_works() {
let input = "30373\n25512\n65332\n33549\n35390";
assert_eq!(y22d08(input, 1), 21);
assert_eq!(y22d08(input, 2), 8);
}
#[test]
fn the_solution() {
let contents = fs::read_to_string("input/2022/day08.txt").unwrap();
assert_eq!(y22d08(&contents, 1), 1807);
assert_eq!(y22d08(&contents, 2), 480000);
}
}