From fef43080703fc49a8ef264b7730be5e899b91b72 Mon Sep 17 00:00:00 2001 From: Riari <3583774+Riari@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:35:16 +0000 Subject: [PATCH] Complete day 17 --- data/examples/17.txt | 13 ++++ data/puzzles/17.md | 128 ++++++++++++++++++++++++++++++++ src/bin/17.rs | 171 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 data/examples/17.txt create mode 100644 data/puzzles/17.md create mode 100644 src/bin/17.rs diff --git a/data/examples/17.txt b/data/examples/17.txt new file mode 100644 index 0000000..f400d6e --- /dev/null +++ b/data/examples/17.txt @@ -0,0 +1,13 @@ +2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533 diff --git a/data/puzzles/17.md b/data/puzzles/17.md new file mode 100644 index 0000000..8bfb4bf --- /dev/null +++ b/data/puzzles/17.md @@ -0,0 +1,128 @@ +\--- Day 17: Clumsy Crucible --- +---------- + +The lava starts flowing rapidly once the Lava Production Facility is operational. As you leave, the reindeer offers you a parachute, allowing you to quickly reach Gear Island. + +As you descend, your bird's-eye view of Gear Island reveals why you had trouble finding anyone on your way up: half of Gear Island is empty, but the half below you is a giant factory city! + +You land near the gradually-filling pool of lava at the base of your new *lavafall*. Lavaducts will eventually carry the lava throughout the city, but to make use of it immediately, Elves are loading it into large [crucibles](https://en.wikipedia.org/wiki/Crucible) on wheels. + +The crucibles are top-heavy and pushed by hand. Unfortunately, the crucibles become very difficult to steer at high speeds, and so it can be hard to go in a straight line for very long. + +To get Desert Island the machine parts it needs as soon as possible, you'll need to find the best way to get the crucible *from the lava pool to the machine parts factory*. To do this, you need to minimize *heat loss* while choosing a route that doesn't require the crucible to go in a *straight line* for too long. + +Fortunately, the Elves here have a map (your puzzle input) that uses traffic patterns, ambient temperature, and hundreds of other parameters to calculate exactly how much heat loss can be expected for a crucible entering any particular city block. + +For example: + +``` +2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533 + +``` + +Each city block is marked by a single digit that represents the *amount of heat loss if the crucible enters that block*. The starting point, the lava pool, is the top-left city block; the destination, the machine parts factory, is the bottom-right city block. (Because you already start in the top-left block, you don't incur that block's heat loss unless you leave that block and then return to it.) + +Because it is difficult to keep the top-heavy crucible going in a straight line for very long, it can move *at most three blocks* in a single direction before it must turn 90 degrees left or right. The crucible also can't reverse direction; after entering each city block, it may only turn left, continue straight, or turn right. + +One way to *minimize heat loss* is this path: + +``` +2>>34^>>>1323 +32v>>>35v5623 +32552456v>>54 +3446585845v52 +4546657867v>6 +14385987984v4 +44578769877v6 +36378779796v> +465496798688v +456467998645v +12246868655 + +``` + +This path never moves more than three consecutive blocks in the same direction and incurs a heat loss of only `*102*`. + +Directing the crucible from the lava pool to the machine parts factory, but not moving more than three consecutive blocks in the same direction, *what is the least heat loss it can incur?* + +Your puzzle answer was `1004`. + +\--- Part Two --- +---------- + +The crucibles of lava simply aren't large enough to provide an adequate supply of lava to the machine parts factory. Instead, the Elves are going to upgrade to *ultra crucibles*. + +Ultra crucibles are even more difficult to steer than normal crucibles. Not only do they have trouble going in a straight line, but they also have trouble turning! + +Once an ultra crucible starts moving in a direction, it needs to move *a minimum of four blocks* in that direction before it can turn (or even before it can stop at the end). However, it will eventually start to get wobbly: an ultra crucible can move a maximum of *ten consecutive blocks* without turning. + +In the above example, an ultra crucible could follow this path to minimize heat loss: + +``` +2>>>>>>>>1323 +32154535v5623 +32552456v4254 +34465858v5452 +45466578v>>>> +143859879845v +445787698776v +363787797965v +465496798688v +456467998645v +122468686556v +254654888773v +432267465553v + +``` + +In the above example, an ultra crucible would incur the minimum possible heat loss of `*94*`. + +Here's another example: + +``` +111111111111 +999999999991 +999999999991 +999999999991 +999999999991 + +``` + +Sadly, an ultra crucible would need to take an unfortunate path like this one: + +``` +1>>>>>>>1111 +9999999v9991 +9999999v9991 +9999999v9991 +9999999v>>>> + +``` + +This route causes the ultra crucible to incur the minimum possible heat loss of `*71*`. + +Directing the *ultra crucible* from the lava pool to the machine parts factory, *what is the least heat loss it can incur?* + +Your puzzle answer was `1171`. + +Both parts of this puzzle are complete! They provide two gold stars: \*\* + +At this point, you should [return to your Advent calendar](/2023) and try another puzzle. + +If you still want to see it, you can [get your puzzle input](17/input). + +You can also [Shareon [Twitter](https://twitter.com/intent/tweet?text=I%27ve+completed+%22Clumsy+Crucible%22+%2D+Day+17+%2D+Advent+of+Code+2023&url=https%3A%2F%2Fadventofcode%2Ecom%2F2023%2Fday%2F17&related=ericwastl&hashtags=AdventOfCode) [Mastodon](javascript:void(0);)] this puzzle. \ No newline at end of file diff --git a/src/bin/17.rs b/src/bin/17.rs new file mode 100644 index 0000000..e54d5fd --- /dev/null +++ b/src/bin/17.rs @@ -0,0 +1,171 @@ +use std::cmp::Ordering; +use std::collections::{BinaryHeap, HashMap}; + +advent_of_code::solution!(17); + +type Grid = Vec>; +type Position = (usize, usize); + +#[derive(Clone, Eq, PartialEq, Hash)] +enum Direction { + North, + East, + South, + West, +} + +impl Direction { + fn get_offset(&self) -> (isize, isize) { + match self { + Direction::North => (0, -1), + Direction::East => (1, 0), + Direction::South => (0, 1), + Direction::West => (-1, 0), + } + } + + fn opposite(&self) -> Self { + match self { + Direction::North => Direction::South, + Direction::East => Direction::West, + Direction::South => Direction::North, + Direction::West => Direction::East, + } + } +} + +#[derive(Clone, Eq, PartialEq, Hash)] +struct State { + position: Position, + entered_from: Direction, + straight_steps: u32, +} + +#[derive(Clone, Eq, PartialEq, Hash)] +struct Step { + state: State, + cost: u32, +} + +impl Ord for Step { + fn cmp(&self, other: &Self) -> Ordering { + other.cost.cmp(&self.cost) + .then_with(|| other.state.position.cmp(&self.state.position)) + } +} + +impl PartialOrd for Step { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Step { + fn new(state: State, cost: u32) -> Self { + Self { + state, + cost, + } + } +} + +impl State { + fn new(position: Position, entered_from: Direction, straight_steps: u32) -> Self { + Self { + position, + entered_from, + straight_steps, + } + } + + fn can_go(&self, direction: &Direction, grid: &Grid) -> bool { + let modifier = direction.get_offset(); + let (x, y) = (self.position.0 as isize + modifier.0, self.position.1 as isize + modifier.1); + x >= 0 && y >= 0 && x < grid[0].len() as isize && y < grid.len() as isize + } +} + +// Implementation of Dijkstra's algorithm that considers steps taken in a direction as well as path cost. +fn solve(input: &str, min_straight_steps: u32, max_straight_steps: u32) -> Option { + let grid: Grid = input.lines().map(|l| l.chars().map(|c| c.to_digit(10).unwrap()).collect()).collect(); + + let start = (0, 0); + let end = (grid[0].len() - 1, grid.len() - 1); + + let mut costs: HashMap = HashMap::new(); + let mut heap: BinaryHeap = BinaryHeap::new(); + + let directions = vec![Direction::North, Direction::East, Direction::South, Direction::West]; + + costs.insert(State { position: start, entered_from: Direction::East, straight_steps: 0 }, 0); + costs.insert(State { position: start, entered_from: Direction::South, straight_steps: 0 }, 0); + heap.push(Step::new(State::new(start, Direction::East, 0), 0)); + + while let Some(Step { state, cost }) = heap.pop() { + if state.position == end && state.straight_steps >= min_straight_steps { + return Some(cost); + } + + if costs.contains_key(&state) && costs[&state] > cost { + continue; + } + + for direction in directions.iter() { + if *direction == state.entered_from.opposite() { + continue; + } + + if !state.can_go(direction, &grid) { + continue; + } + + if state.straight_steps == max_straight_steps && *direction == state.entered_from { + continue; + } + + let (offset_x, offset_y) = direction.get_offset(); + let new_position = ((state.position.0 as isize + offset_x) as usize, (state.position.1 as isize + offset_y) as usize); + let neighbour = Step::new( + State::new( + new_position, + direction.clone(), + if *direction == state.entered_from { state.straight_steps + 1 } else { 1 }, + ), + cost + grid[new_position.1][new_position.0], + ); + + if (*direction == state.entered_from || state.straight_steps >= min_straight_steps) + && neighbour.cost < *costs.get(&neighbour.state).unwrap_or(&u32::MAX) { + heap.push(neighbour.clone()); + costs.insert(neighbour.state, neighbour.cost); + } + } + } + + None +} + +pub fn part_one(input: &str) -> Option { + solve(input, 0, 3) +} + +pub fn part_two(input: &str) -> Option { + solve(input, 4, 10) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + let result = part_one(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(102)); + } + + #[test] + fn test_part_two() { + let result = part_two(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(94)); + } +}