Skip to content

Commit

Permalink
Complete day 17
Browse files Browse the repository at this point in the history
  • Loading branch information
Riari committed Dec 18, 2023
1 parent 8a298b1 commit fef4308
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 0 deletions.
13 changes: 13 additions & 0 deletions data/examples/17.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533
128 changes: 128 additions & 0 deletions data/puzzles/17.md
Original file line number Diff line number Diff line change
@@ -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<v
25465488877v5
43226746555v>
```

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.
171 changes: 171 additions & 0 deletions src/bin/17.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};

advent_of_code::solution!(17);

type Grid = Vec<Vec<u32>>;
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<Ordering> {
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<u32> {
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<State, u32> = HashMap::new();
let mut heap: BinaryHeap<Step> = 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<u32> {
solve(input, 0, 3)
}

pub fn part_two(input: &str) -> Option<u32> {
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));
}
}

0 comments on commit fef4308

Please sign in to comment.