this post was submitted on 17 Dec 2023
0 points (NaN% liked)

Advent Of Code

767 readers
1 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2023

Solution Threads

M T W T F S S
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

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
 

Day 17: Clumsy Crucible

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

top 2 comments
sorted by: hot top controversial new old
[โ€“] hades@lemm.ee 1 points 9 months ago* (last edited 1 month ago)

Python

749 line-seconds

import collections
import dataclasses
import heapq

import numpy as np

from .solver import Solver


@dataclasses.dataclass(order=True)
class QueueEntry:
  price: int
  x: int
  y: int
  momentum_x: int
  momentum_y: int
  deleted: bool


class Day17(Solver):
  lines: list[str]
  sx: int
  sy: int
  lower_bounds: np.ndarray

  def __init__(self):
    super().__init__(17)

  def presolve(self, input: str):
    self.lines = input.splitlines()
    self.sx = len(self.lines[0])
    self.sy = len(self.lines)
    start = (self.sx - 1, self.sy - 1)
    self.lower_bounds = np.zeros((self.sx, self.sy)) + np.inf
    self.lower_bounds[start] = 0
    queue: list[QueueEntry] = [QueueEntry(0, self.sx - 1, self.sy - 1, 0, 0, False)]
    queue_entries: dict[tuple[int, int], QueueEntry] = {start: queue[0]}
    while queue:
      cur_price, x, y, _, _, deleted = dataclasses.astuple(heapq.heappop(queue))
      if deleted:
        continue
      del queue_entries[(x, y)]
      self.lower_bounds[x, y] = cur_price
      price = cur_price + int(self.lines[y][x])
      for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
        nx, ny = x + dx, y + dy
        if not (0 <= nx < self.sx) or not (0 <= ny < self.sy):
          continue
        if price < self.lower_bounds[nx, ny]:
          self.lower_bounds[nx, ny] = price
          if (nx, ny) in queue_entries:
            queue_entries[(nx, ny)].deleted = True
          queue_entries[(nx, ny)] = QueueEntry(price, nx, ny, 0, 0, False)
          heapq.heappush(queue, queue_entries[(nx, ny)])

  def _solve(self, maximum_run: int, minimum_run_to_turn: int):
    came_from: dict[tuple[int, int, int, int], tuple[int, int, int, int]] = {}
    start = (0, 0, 0, 0)
    queue: list[QueueEntry] = [QueueEntry(self.lower_bounds[0, 0], *start, False)]
    queue_entries: dict[tuple[int, int, int, int], QueueEntry] = {start: queue[0]}
    route: list[tuple[int, int]] = []
    prices: dict[tuple[int, int, int, int], float] = collections.defaultdict(lambda: np.inf)
    prices[start] = 0
    while queue:
      _, current_x, current_y, momentum_x, momentum_y, deleted = dataclasses.astuple(heapq.heappop(queue))
      cur_price = prices[(current_x, current_y, momentum_x, momentum_y)]
      if deleted:
        continue
      if ((current_x, current_y) == (self.sx - 1, self.sy - 1) and
          (momentum_x >= minimum_run_to_turn or momentum_y >= minimum_run_to_turn)):
        previous = came_from.get((current_x, current_y, momentum_x, momentum_y))
        route.append((current_x, current_y))
        while previous:
          x, y, *_ = previous
          if x != 0 or y != 0:
            route.append((x, y))
          previous = came_from.get(previous)
        break
      for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
        dot_product = dx * momentum_x + dy * momentum_y
        if dot_product < 0 or dot_product >= maximum_run:
          continue
        if ((momentum_x or momentum_y) and dot_product == 0 and
            abs(momentum_x) < minimum_run_to_turn and abs(momentum_y) < minimum_run_to_turn):
          continue
        new_x, new_y = current_x + dx, current_y + dy
        if not (0 <= new_x < self.sx) or not (0 <= new_y < self.sy):
          continue
        new_momentum_x, new_momentum_y = (dx, dy) if dot_product == 0 else (momentum_x + dx, momentum_y + dy)
        new_position = (new_x, new_y, new_momentum_x, new_momentum_y)
        potential_new_price = cur_price + int(self.lines[new_y][new_x])
        if potential_new_price < prices[new_position]:
          queue_entry = queue_entries.get(new_position)
          if queue_entry:
            queue_entry.deleted = True
          queue_entries[new_position] = QueueEntry(potential_new_price + self.lower_bounds[new_x, new_y],
                                                   *new_position, False)
          came_from[new_position] = (current_x, current_y, momentum_x, momentum_y)
          prices[new_position] = potential_new_price
          heapq.heappush(queue, queue_entries[new_position])
    return sum(int(self.lines[y][x]) for x, y in route)

  def solve_first_star(self) -> int:
    return self._solve(3, 0)

  def solve_second_star(self) -> int:
    return self._solve(10, 4)
[โ€“] Gobbel2000@feddit.de 0 points 9 months ago

Rust

I used A*. Having to keep track of the direction you're going (horizontal or vertical) made things a bit more tricky. But the trickiest part was me writing pos - 1 instead of pos - i which didn't affect the test input, but on the real input that caused the result to be one too high. That's the fun kind of mistake.

Part 2 runs in 18ms.

use std::cmp::Ordering;
use std::ops::RangeInclusive;
use std::collections::BinaryHeap;

use ndarray::Array2;

fn parse_input(input: String) -> Array2<u8> {
    let lines: Vec<_> = input.lines().collect();
    let n_lines = lines.len();
    let elements: Vec<_> = lines.iter().flat_map(|l|
            l.chars().map(|c| c.to_digit(10).unwrap() as u8)
        )
        .collect();
    Array2::from_shape_vec((n_lines, elements.len() / n_lines), elements)
        .unwrap()
}

#[derive(Copy, Clone, Eq, PartialEq)]
struct State {
    cost: u32,
    position: Position,
}

impl Ord for State {
    fn cmp(&self, other: &Self) -> Ordering {
        // Flip ordering to get min-heap instead of max-heap
        other.cost.cmp(&self.cost)
            .then_with(|| self.position.cmp(&other.position))
    }
}

impl PartialOrd for State {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
struct Position {
    position: (usize, usize),
    horizontal: bool,
}

impl Position {
    fn adj(&self, field: &Array2<u8>, moves: RangeInclusive<usize>) -> Vec<(Self, u32)> {
        let (pos, weights) = if self.horizontal {
            (self.position.0, field.column(self.position.1))
        } else {
            (self.position.1, field.row(self.position.0))
        };
        let mut positions = Vec::new();
        let mut weight: u32 = 0;
        for i in 1..=*moves.end() {
            if pos < i {
                break;
            }
            let pos2 = pos - i;
            weight += weights[pos2] as u32;
            if i >= *moves.start() {
                let new_pos = if self.horizontal {
                    (pos2, self.position.1)
                } else {
                    (self.position.0, pos2)
                };
                positions.push((Position { position: new_pos, horizontal: !self.horizontal }, weight));
            }
        }
        weight = 0;
        for i in 1..=*moves.end() {
            let pos2 = pos + i;
            if pos2 >= weights.len() {
                break;
            }
            weight += weights[pos2] as u32;
            if i >= *moves.start() {
                let new_pos = if self.horizontal {
                    (pos2, self.position.1)
                } else {
                    (self.position.0, pos2)
                };
                positions.push((Position { position: new_pos, horizontal: !self.horizontal }, weight));
            }
        }
        positions
    }

    // Index into dist array
    fn key(&self, field: &Array2<u8>) -> usize {
        (self.position.0 * field.ncols() + self.position.1) * 2
            + self.horizontal as usize
    }

    // Manhattan distance as heuristic for A*
    fn heuristic(&self, end: (usize, usize)) -> u32 {
        ((end.0 - self.position.0) + (end.1 - self.position.1)) as u32
    }
}

fn astar(field: Array2<u8>, moves: RangeInclusive<usize>) -> Option<u32> {
    let start = (0, 0);
    let end = (field.nrows() - 1, field.ncols() - 1,);
    
    let mut dist = vec![u32::MAX; field.len() * 2];
    // Try starting in both directions
    let starts = [
        Position { position: start, horizontal: false },
        Position { position: start, horizontal: true },
    ];
    dist[starts[0].key(&field)] = 0;
    dist[starts[1].key(&field)] = 0;
    let mut heap = BinaryHeap::from([
        State { cost: starts[0].heuristic(end), position: starts[0] },
        State { cost: starts[1].heuristic(end), position: starts[1] },
    ]);

    while let Some(State { cost, position }) = heap.pop() {
        if position.position == end {
            return Some(cost)
        }
        for (next, weight) in position.adj(&field, moves.clone()) {
            let new_dist = dist[position.key(&field)] + weight;
            if new_dist < dist[next.key(&field)] {
                dist[next.key(&field)] = new_dist;
                heap.push(State {
                    cost: new_dist + next.heuristic(end),
                    position: next,
                });
            }
        }
    }
    // End unreachable
    None
}

fn part1(input: String) {
    let field = parse_input(input);    
    println!("{}", astar(field, 1..=3).unwrap());
}

fn part2(input: String) {
    let field = parse_input(input);    
    println!("{}", astar(field, 4..=10).unwrap());
}

util::aoc_main!("day17.txt");