Metadata
-
Date
-
Tagged
-
Part of series
- Advent of Code 2024 Day 1
- Advent of Code 2024 Day 2
- Advent of Code 2024 Day 3
- Advent of Code 2024 Day 4
- Advent of Code 2024 Day 5
- Advent of Code 2024 Day 6
- Advent of Code 2024 Day 7
- Advent of Code 2024 Day 8
- Advent of Code 2024 Day 9
- Advent of Code 2024 Day 10
- Advent of Code 2024 Day 11
- Advent of Code 2024 Day 12
- Advent of Code 2024 Day 13
- Advent of Code 2024 Day 14
- Advent of Code 2024 Day 15
- Advent of Code 2024 Day 16
- Advent of Code 2024 Day 17
- Advent of Code 2024 Day 18
- Advent of Code 2024 Day 19
- Advent of Code 2024 Day 20
- Advent of Code 2024 Day 21
- Advent of Code 2024 Day 22
- Advent of Code 2024 Day 23
- Advent of Code 2024 Day 24
- Advent of Code 2024 Day 25
-
Older post
-
Newer post
Advent of Code 2024 Day 18
Day 18: RAM Run
https://adventofcode.com/2024/day/18
Another day, another familiar sensation. This time, you’re inside a computer again.
Some algorithm is corrupting bytes in the memory where you are, one byte at a time. That’s today’s input, an ordered list of byte locations the algorithm is going to corrupt. When a position becomes corrupt, you can no longer be at that location.
An example input looks like this:
5,44,24,53,02,16,32,41,50,63,32,65,11,25,52,56,51,40,46,41,16,11,00,51,62,0Each line is a location, in a col_idx,row_idx form.
Wait a minute, this is another 2D-grid problem!
Parsing
Alright, you might already know the drill, time for a Point.
Then turn the input into a list of them.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]struct Point { row: i32, col: i32,}
impl Point { fn new(row: i32, col: i32) -> Self { Self { row, col } }}
fn parse(input: &str) -> Vec<Point> { input .lines() .map(|line| { let (col, row) = line.split_once(',').unwrap(); Point::new(row.parse().unwrap(), col.parse().unwrap()) }) .collect()}Part 1
You need to get out of this place while you still can.
- The grid you are in is 71 by 71 in size (the problem mentions 70, but those are 0-indexed numbers!).
- You start at row:
0, col:0 - The goal is at row:
70, col:70
The question asks what the minimum amount of steps is to reach the goal after 1024 positions have become corrupted.
Each step you can move in the 4 directions if that location isn’t corrupted. This is a breadth-first-search again, so I used some of the helpers from previous days.
Helpers
impl Point { fn new(row: i32, col: i32) -> Self { Self { row, col } }
fn add(&self, other: &Self) -> Self { Self::new(self.row + other.row, self.col + other.col) }
fn dirs() -> [Point; 4] { [ // up Self::new(-1, 0), // right Self::new(0, 1), // down Self::new(1, 0), // left Self::new(0, -1), ] }
fn is_in_bounds(&self, rows: i32, cols: i32) -> bool { (self.row >= 0 && self.row < rows) && (self.col >= 0 && self.col < cols) }
fn neighbours(&self, rows: i32, cols: i32) -> Vec<Self> { let mut neighbours = Vec::new(); for dir in Self::dirs() { let next = self.add(&dir); if next.is_in_bounds(rows, cols) { neighbours.push(next); } } neighbours }}Code
Breadth-first-search, where each item in the queue is a (point, score) pair, and the score increases by 1 each step.
Point neighbours are only considered if they are not corrupted.
Before starting the search, I create a set of the first 1024 points in the input that become corrupted.
const ROWS: i32 = 71;const COLS: i32 = 71;const SIZE: usize = 1024;
fn part_1(input: &str) -> u32 { let falling = parse(input); let corrupted: HashSet<&Point> = falling.iter().take(SIZE).collect();
let start = Point::new(0, 0); let end = Point::new(ROWS - 1, COLS - 1);
let mut q = VecDeque::new(); let mut seen = HashSet::new(); q.push_back((start, 0)); seen.insert(start);
while let Some((pos, cost)) = q.pop_front() { if pos == end { return cost; } for neighbour in pos.neighbours(ROWS, COLS) { if corrupted.contains(&neighbour) { continue; } if seen.insert(neighbour) { q.push_back((neighbour, cost + 1)); } } }
panic!("at the disco");}Part 2
The historians want to know how slow they can go before a byte is corrupted that makes it impossible to reach the end from the start. I refactored my code from part1 a bit so the search logic is its own function that either returns a cost, or nothing. If it returns nothing, there is no possible path from start to finish.
The helper takes in a set of locations that are corrupted.
I then used that helper and performed a binary search on the list of input positions. At some position, crossing from start to end becomes impossible.
- Everything before that point: a path exists
- Everything after that point: no path exists
Helpers
fn search( corrupted: &HashSet<&Point>, start: Point, end: Point, rows: i32, cols: i32,) -> Option<u32> { let mut q = VecDeque::new(); let mut seen = HashSet::new(); q.push_back((start, 0)); seen.insert(start);
while let Some((pos, cost)) = q.pop_front() { if pos == end { return Some(cost); } for neighbour in pos.neighbours(rows, cols) { if corrupted.contains(&neighbour) { continue; } if seen.insert(neighbour) { q.push_back((neighbour, cost + 1)); } } }
None}Code
fn part_2(input: &str) -> String { let falling = parse(input);
let start = Point::new(0, 0); let end = Point::new(ROWS - 1, COLS - 1);
let mut low = 0; let mut high = falling.len() - 1;
while low < high { let mid = (low + high) / 2; let corrupted: HashSet<&Point> = falling.iter().take(mid + 1).collect(); if search(&corrupted, start, end, ROWS, COLS).is_some() { low = mid + 1; } else { high = mid; } }
let last = falling[low]; format!("{},{}", last.col, last.row)}Final code
use std::collections::{HashSet, VecDeque};
const ROWS: i32 = 71;const COLS: i32 = 71;const SIZE: usize = 1024;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]struct Point { row: i32, col: i32,}
impl Point { fn new(row: i32, col: i32) -> Self { Self { row, col } }
fn add(&self, other: &Self) -> Self { Self::new(self.row + other.row, self.col + other.col) }
fn dirs() -> [Point; 4] { [ // up Self::new(-1, 0), // right Self::new(0, 1), // down Self::new(1, 0), // left Self::new(0, -1), ] }
fn is_in_bounds(&self, rows: i32, cols: i32) -> bool { (self.row >= 0 && self.row < rows) && (self.col >= 0 && self.col < cols) }
fn neighbours(&self, rows: i32, cols: i32) -> Vec<Self> { let mut neighbours = Vec::new(); for dir in Self::dirs() { let next = self.add(&dir); if next.is_in_bounds(rows, cols) { neighbours.push(next); } } neighbours }}
fn parse(input: &str) -> Vec<Point> { input .lines() .map(|line| { let (col, row) = line.split_once(',').unwrap(); Point::new(row.parse().unwrap(), col.parse().unwrap()) }) .collect()}
fn part_1(input: &str) -> u32 { let falling = parse(input); let corrupted: HashSet<&Point> = falling.iter().take(SIZE).collect();
let start = Point::new(0, 0); let end = Point::new(ROWS - 1, COLS - 1);
search(&corrupted, start, end, ROWS, COLS).unwrap()}
fn search( corrupted: &HashSet<&Point>, start: Point, end: Point, rows: i32, cols: i32,) -> Option<u32> { let mut q = VecDeque::new(); let mut seen = HashSet::new(); q.push_back((start, 0)); seen.insert(start);
while let Some((pos, cost)) = q.pop_front() { if pos == end { return Some(cost); } for neighbour in pos.neighbours(rows, cols) { if corrupted.contains(&neighbour) { continue; } if seen.insert(neighbour) { q.push_back((neighbour, cost + 1)); } } }
None}
fn part_2(input: &str) -> String { let falling = parse(input);
let start = Point::new(0, 0); let end = Point::new(ROWS - 1, COLS - 1);
let mut low = 0; let mut high = falling.len() - 1;
while low < high { let mid = (low + high) / 2; let corrupted: HashSet<&Point> = falling.iter().take(mid + 1).collect(); if search(&corrupted, start, end, ROWS, COLS).is_some() { low = mid + 1; } else { high = mid; } }
let last = falling[low]; format!("{},{}", last.col, last.row)}