First idea for day17-part2, takes too long
This commit is contained in:
parent
b75eeed848
commit
8bdf937185
8
2022/day17-part2/Cargo.toml
Normal file
8
2022/day17-part2/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "day17-part2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
345
2022/day17-part2/src/main.rs
Normal file
345
2022/day17-part2/src/main.rs
Normal file
@ -0,0 +1,345 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum RockShape {
|
||||
Minus,
|
||||
Plus,
|
||||
J,
|
||||
I,
|
||||
O,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum FallingDirection {
|
||||
Left,
|
||||
Right,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
struct FallingRock {
|
||||
shape: RockShape,
|
||||
// Coordinates of the falling rock identifying the bottom left piece of it.
|
||||
// For the '+'-shape this actually refers to a piece of air.
|
||||
// y counts up from the bottom layer 0 up to infinity.
|
||||
// x counts up from left 0 to right 6.
|
||||
x: usize,
|
||||
y: usize,
|
||||
}
|
||||
|
||||
// To allow easy iteration over all coordinates of a falling rock
|
||||
// I'm providing a custom iterator here.
|
||||
#[derive(Debug)]
|
||||
struct FallingRockIterator {
|
||||
fr: FallingRock,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl Iterator for FallingRockIterator {
|
||||
type Item = (usize, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// The offsets for the different shapes, stored statically.
|
||||
let offsets: &'static [(usize, usize)] = match &self.fr.shape {
|
||||
RockShape::Minus => &[(0, 0), (1, 0), (2, 0), (3, 0)],
|
||||
RockShape::Plus => &[(0, 1), (1, 0), (1, 1), (1, 2), (2, 1)],
|
||||
RockShape::J => &[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2)],
|
||||
RockShape::I => &[(0, 0), (0, 1), (0, 2), (0, 3)],
|
||||
RockShape::O => &[(0, 0), (0, 1), (1, 0), (1, 1)],
|
||||
};
|
||||
// The actual calculation, where we apply rock coordinate + offset at current index i.
|
||||
// Yields None if i has already iterated over everything.
|
||||
if self.i < offsets.len() {
|
||||
// Don't forget to increment the offset index.
|
||||
let old_i = self.i;
|
||||
self.i += 1;
|
||||
Some((self.fr.x + offsets[old_i].0, self.fr.y + offsets[old_i].1))
|
||||
} else {
|
||||
// No incrementing the offset index once we've reached the end.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FallingRock {
|
||||
// Construct a FallingRockIterator allowing us to iterate over all coordinates of this rock.
|
||||
fn iter(&self) -> FallingRockIterator {
|
||||
FallingRockIterator { fr: *self, i: 0 }
|
||||
}
|
||||
|
||||
// Simulate the falling rock within the existing cave,
|
||||
// and attempt to move it in the specified direction.
|
||||
// Left and Right will either return a moved FallingRock,
|
||||
// or return the exact same FallingRock if moving was not possible.
|
||||
// Down will either return a moved FallingRock or
|
||||
// or consume the FallingRock and settle it as static Rock-tiles within the Cave.
|
||||
//
|
||||
// If the FallingRock has been settled, None will be returned.
|
||||
// Otherwise, the (possibly moved) Some(FallingRock) will be returned.
|
||||
fn attempt_move(mut self, cave: &mut Cave, dir: FallingDirection) -> Option<FallingRock> {
|
||||
match dir {
|
||||
FallingDirection::Left => {
|
||||
// In order to move the piece ...
|
||||
// (1) it must not touch the left wall
|
||||
// (2) all the tiles to the left of it must be air
|
||||
if self.x > 0 && self.iter().all(|(x, y)| !cave.is_rock(x - 1, y)) {
|
||||
self.x -= 1;
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
FallingDirection::Right => {
|
||||
// In order to move the piece ...
|
||||
// (1) it must not touch the right wall
|
||||
// (2) all the tiles to the right of it must be air
|
||||
if self.iter().map(|e| e.0).max().unwrap() < 6
|
||||
&& self.iter().all(|(x, y)| !cave.is_rock(x + 1, y))
|
||||
{
|
||||
self.x += 1;
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
FallingDirection::Down => {
|
||||
// If we've reached the bottom of the cave we have to settle.
|
||||
// If any of the tiles below the current piece are rocks, we also have to settle.
|
||||
if self.y == 0 || self.iter().any(|(x, y)| cave.is_rock(x, y - 1)) {
|
||||
// Ensure the cave itself can hold all the possible coordinates.
|
||||
// We're adding one extra b/c the topmost line should always be air-only.
|
||||
cave.extend_to(self.y + 4);
|
||||
// Actually settle the rock.
|
||||
for (x, y) in self.iter() {
|
||||
cave.set(x, y, Tile::Rock);
|
||||
}
|
||||
// Consume it.
|
||||
None
|
||||
} else {
|
||||
// Since we're not settling, we *are* moving.
|
||||
self.y -= 1;
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum Tile {
|
||||
Air,
|
||||
Rock,
|
||||
}
|
||||
|
||||
// Grows upwards, i.e. the "back" is the "top" of the stack.
|
||||
struct Cave {
|
||||
// The actual data - a double-ended queue / ring-buffer of lines.
|
||||
data: VecDeque<[Tile; 7]>,
|
||||
// In order to be able to compute to 1 trillion, as required for part 2,
|
||||
// we're going to regularly clean up "garbage" from the bottom of the tower
|
||||
// that is no longer needed for the simulation.
|
||||
// Nonetheless, we have to keep track of the current floor coordinate:
|
||||
floor: usize,
|
||||
}
|
||||
|
||||
impl Cave {
|
||||
// Read an arbitrary coordinate within the cave.
|
||||
// This uses the simulated coordinates,
|
||||
// i.e. the y coordinate can become *incredibly* large here.
|
||||
fn get(&self, x: usize, y: usize) -> Tile {
|
||||
self.data[y - self.floor][x]
|
||||
}
|
||||
|
||||
// Write to an arbitrary coordinate within the cave.
|
||||
// This uses the simulated coordinates,
|
||||
// i.e. the y coordinate can become *incredibly* large here.
|
||||
fn set(&mut self, x: usize, y: usize, val: Tile) {
|
||||
self.data[y - self.floor][x] = val;
|
||||
}
|
||||
|
||||
// Get the simulated size of the tower.
|
||||
fn height(&self) -> usize {
|
||||
self.data.len() + self.floor
|
||||
}
|
||||
|
||||
// Print the cave to stdout.
|
||||
// You can optionally provide a falling rock to print as well.
|
||||
#[allow(dead_code)]
|
||||
fn print(&self, falling_rock: &Option<FallingRock>) {
|
||||
for y in (self.floor..(self.height() + 10)).rev() {
|
||||
print!("|");
|
||||
for x in 0..7 {
|
||||
// Air is the default tile.
|
||||
let mut tile = '.';
|
||||
// If the cave has data for this coordinate, check if it's a settled rock.
|
||||
if y < self.height() && self.get(x, y) == Tile::Rock {
|
||||
tile = '#';
|
||||
}
|
||||
// Next, check if a falling rock exists here and override the tile with '@' if so.
|
||||
if let Some(ref fr) = falling_rock {
|
||||
if fr.iter().any(|(cx, cy)| cx == x && cy == y) {
|
||||
tile = '@';
|
||||
}
|
||||
}
|
||||
// Print the determined tile.
|
||||
print!("{}", tile);
|
||||
}
|
||||
println!("|");
|
||||
}
|
||||
if self.floor == 0 {
|
||||
println!("+-------+\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if a given simulated coordinate refers to a settled rock.
|
||||
// Also takes into account coordinates that extend beyond the current length of the Vec.
|
||||
fn is_rock(&self, x: usize, y: usize) -> bool {
|
||||
if y >= self.height() {
|
||||
false
|
||||
} else {
|
||||
self.get(x, y) == Tile::Rock
|
||||
}
|
||||
}
|
||||
|
||||
// Vector not big enough? Ensure that it is big enough for the passed-along y-coordinate.
|
||||
fn extend_to(&mut self, y: usize) {
|
||||
// Simply push 7-element arrays of Air-tiles
|
||||
// onto the Vector until our condition is satisfied.
|
||||
while y >= self.height() {
|
||||
self.data.push_back([Tile::Air; 7]);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the simulated y-coordinate of the first rock-free line at the top of the tower.
|
||||
fn past_the_top(&self) -> usize {
|
||||
// Iterate through all data-lines, starting from the top.
|
||||
// Find the first line where there is at least one rock.
|
||||
// Then, return the y-coordinate that is one bigger, i.e. the previous, air-only line.
|
||||
// If no line could be found we assume we're at the start of simulation
|
||||
// where y=0 is the first free line.
|
||||
self.data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, line)| line.iter().any(|e| e == &Tile::Rock))
|
||||
.map_or(0, |(i, _)| i + 1 + self.floor)
|
||||
}
|
||||
|
||||
// In order to pull off 1 trillion lines we have to regularly clean up "garbage"
|
||||
// at the bottom of the tower that isn't needed anymore for a correct simulation.
|
||||
// We do this by finding, starting from the top of the tower, the first two lines
|
||||
// that, when "OR"ed together, yield a wall.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// Line A: |##....#| |#.#.#.#| |##.....|
|
||||
// Line B: |.#####.| |.#.#.#.| |...####|
|
||||
//
|
||||
// Yields: |#######| |#######| |##.####|
|
||||
// Therefore: OK OK CONTINUE
|
||||
//
|
||||
// Once located, anything below the lower of those two lines can be removed.
|
||||
// Once cut off the `floor` of the cave has to be incremented accordingly.
|
||||
fn collect_garbage(&mut self) {
|
||||
// Zip up a reverse iterator with another reverse-iterator that skips the topmost line
|
||||
// to iterate over all pairs of lines.
|
||||
let dy = self
|
||||
.data
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(self.data.iter().enumerate().rev().skip(1))
|
||||
.find(|(a, (_, b))| {
|
||||
// Find the first pair where every column contains at least one rock.
|
||||
a.iter()
|
||||
.zip(b.iter())
|
||||
.all(|(ea, eb)| ea == &Tile::Rock || eb == &Tile::Rock)
|
||||
})
|
||||
// We're interested in the coordinate, so map that out.
|
||||
.map(|(_, (y, _))| y);
|
||||
|
||||
// Didn't find a wall? That's fine, no garbage to clean up then.
|
||||
if let Some(dy) = dy {
|
||||
// Drain the range from the bottom and drop the iterator.
|
||||
// This frees all the elements at the front of the VecDeque in bulk.
|
||||
drop(self.data.drain(0..dy));
|
||||
// Don't forget to adjust the floor.
|
||||
self.floor += dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Use command line arguments to specify the input filename.
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 2 {
|
||||
panic!("Usage: ./main <input-file>\nNo input file provided. Exiting.");
|
||||
}
|
||||
|
||||
// Next, read the contents of the input file into a string for easier processing.
|
||||
let input = std::fs::read_to_string(&args[1]).expect("Error opening file");
|
||||
|
||||
// Create an infinitely-looping iterator for the input directions.
|
||||
// We're also filtering out any characters that aren't '<' or '>' such as newlines
|
||||
// and are simulatenously mapping '<' and '>' to FallingDirection::Left and ::Right respectively.
|
||||
let mut input_directions = input
|
||||
.chars()
|
||||
.filter_map(|e| match e {
|
||||
'<' => Some(FallingDirection::Left),
|
||||
'>' => Some(FallingDirection::Right),
|
||||
_ => None,
|
||||
})
|
||||
.cycle();
|
||||
|
||||
// Also create an infinitely-looping iterator for the rock-types.
|
||||
let mut rock_shapes = [
|
||||
RockShape::Minus,
|
||||
RockShape::Plus,
|
||||
RockShape::J,
|
||||
RockShape::I,
|
||||
RockShape::O,
|
||||
]
|
||||
.iter()
|
||||
.cycle();
|
||||
|
||||
// The cave where all the rocks will settle.
|
||||
let mut cave = Cave {
|
||||
data: VecDeque::new(),
|
||||
floor: 0,
|
||||
};
|
||||
|
||||
// Simulate ONE TRILLION rocks.
|
||||
for i in 0..1_000_000_000_000usize {
|
||||
// Create the next falling rock.
|
||||
let mut fr = Some(FallingRock {
|
||||
shape: *rock_shapes.next().unwrap(),
|
||||
// Always two spaces from the left wall.
|
||||
x: 2,
|
||||
// Always three lines of free space.
|
||||
y: cave.past_the_top() + 3,
|
||||
});
|
||||
|
||||
// Every 1k rocks attempt to clean up garbage.
|
||||
if i % 1000 == 0 {
|
||||
cave.collect_garbage();
|
||||
}
|
||||
|
||||
if i % 1_000_000 == 0 {
|
||||
println!("Remaining: {}", 1_000_000_000_000 - i);
|
||||
}
|
||||
|
||||
// Keep moving l/r and down until the rock settles.
|
||||
loop {
|
||||
// Move left / right.
|
||||
let dir = input_directions.next().unwrap();
|
||||
fr = fr.unwrap().attempt_move(&mut cave, dir);
|
||||
|
||||
// Next, move down.
|
||||
fr = fr.unwrap().attempt_move(&mut cave, FallingDirection::Down);
|
||||
// Did it settle? If so, move to the next rock.
|
||||
if fr.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Topmost free y-coordinate after 2022 rocks have settled: {}",
|
||||
cave.past_the_top()
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user