Solution for 2022/day17-part2, now computing in time

This commit is contained in:
Tobias Marschner 2024-03-31 15:54:58 +02:00
parent 8bdf937185
commit 8b7799d71a

View File

@ -128,6 +128,7 @@ enum Tile {
}
// Grows upwards, i.e. the "back" is the "top" of the stack.
#[derive(Debug, Clone)]
struct Cave {
// The actual data - a double-ended queue / ring-buffer of lines.
data: VecDeque<[Tile; 7]>,
@ -183,8 +184,9 @@ impl Cave {
println!("|");
}
if self.floor == 0 {
println!("+-------+\n");
println!("+-------+");
}
println!("\n");
}
// Checks if a given simulated coordinate refers to a settled rock.
@ -236,7 +238,9 @@ impl Cave {
//
// 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) {
//
// Returns true if garbage was collected, false otherwise.
fn collect_garbage(&mut self) -> bool {
// Zip up a reverse iterator with another reverse-iterator that skips the topmost line
// to iterate over all pairs of lines.
let dy = self
@ -260,10 +264,75 @@ impl Cave {
drop(self.data.drain(0..dy));
// Don't forget to adjust the floor.
self.floor += dy;
// Garbage was found and removed.
true
} else {
// No garbage was found.
false
}
}
}
// Store the *entire* state of the system in a struct.
// This includes the entire cave, its floor number and
// the current indices into the shape and direction iterators.
#[derive(Debug)]
struct SystemState {
cave: Cave,
rock_idx: usize,
rocks_in_cave: usize,
shape_idx: usize,
dir_idx: usize,
}
impl SystemState {
// Copy over the current state of the system and adjust the cave-data, deleting any empty
// lines.
fn new(cave: &Cave, rock_idx: usize, shape_idx: usize, dir_idx: usize) -> SystemState {
// Clone the cave for inclusion in the SystemState.
let mut cave = cave.clone();
// To ensure consistency, cut off all empty lines at the top of the cave.
let y = cave
.data
.iter()
.enumerate()
.rev()
.find(|(_, line)| line.iter().any(|e| e == &Tile::Rock))
.map(|(y, _)| y + 1)
.unwrap();
cave.data.truncate(y);
// Collect statistics on the cave for faster comparison.
let rock_count: usize = cave
.data
.iter()
.map(|l| l.iter().filter(|e| e == &&Tile::Rock).count())
.sum();
// Create the new system state and return it.
SystemState {
cave,
rock_idx,
shape_idx,
dir_idx,
rocks_in_cave: rock_count,
}
}
fn is_equal(&self, other: &Self) -> bool {
// To improve performance, check the easy parameters first.
self.rocks_in_cave == other.rocks_in_cave
&& self.shape_idx == other.shape_idx
&& self.dir_idx == other.dir_idx
// Then, check the actual cave layout. For every line in both caves ...
&& self
.cave
.data
.iter()
.zip(other.cave.data.iter())
// ... ensure every tile in each line is identical.
.all(|(al, bl)| al.iter().zip(bl.iter()).all(|(ae, be)| ae == be))
}
}
fn main() {
// Use command line arguments to specify the input filename.
let args: Vec<String> = std::env::args().collect();
@ -284,6 +353,7 @@ fn main() {
'>' => Some(FallingDirection::Right),
_ => None,
})
.enumerate()
.cycle();
// Also create an infinitely-looping iterator for the rock-types.
@ -295,6 +365,7 @@ fn main() {
RockShape::O,
]
.iter()
.enumerate()
.cycle();
// The cave where all the rocks will settle.
@ -303,30 +374,39 @@ fn main() {
floor: 0,
};
// The total collection of distinct states.
// We have to find the loop in the system.
let mut states: Vec<SystemState> = Vec::new();
let mut current_dir_idx: usize;
let mut current_shape_idx: usize;
// We want to fast-forward *once*.
let mut fast_forwarded = false;
// Simulate ONE TRILLION rocks.
for i in 0..1_000_000_000_000usize {
let mut i = 0usize;
const N: usize = 1_000_000_000_000usize;
loop {
// Grab the next shape.
let (shape_idx, shape) = rock_shapes.next().unwrap();
current_shape_idx = shape_idx;
// Create the next falling rock.
let mut fr = Some(FallingRock {
shape: *rock_shapes.next().unwrap(),
shape: *shape,
// 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 {
// Grab the next direction.
let (dir_idx, dir) = input_directions.next().unwrap();
current_dir_idx = dir_idx;
// Move left / right.
let dir = input_directions.next().unwrap();
fr = fr.unwrap().attempt_move(&mut cave, dir);
// Next, move down.
@ -336,6 +416,41 @@ fn main() {
break;
}
}
// Attempt to collect garbage every cycle and
// store the system state if garbage has been collected.
// Only bother with fast-forwarding if we haven't forwarded already.
if cave.collect_garbage() && !fast_forwarded {
// Create the new SystemState.
let s = SystemState::new(&cave, i, current_shape_idx, current_dir_idx);
// Compare it against all old states.
let res = states
.iter()
.rev()
.find(|e| e.is_equal(&s));
// Found the cycle? Excellent. Then fast-forward as much as we can.
if let Some(res_elem) = res {
// We know the indices and cave-makeup from then and now are exactly identicaly.
// Only the rock_idx and floor-value are different.
let rock_delta = s.rock_idx - res_elem.rock_idx;
let floor_delta = s.cave.floor - res_elem.cave.floor;
// Determine by how many rocks we can fast-forward to get as close to N as possible.
let cycles_to_ff = (N - i) / rock_delta;
// Then, actually fast-forward by that number of cycles.
i += cycles_to_ff * rock_delta;
cave.floor += cycles_to_ff * floor_delta;
// Only fast-forward once.
fast_forwarded = true;
}
states.push(s);
// println!("No. of states: {}", states.len());
}
// Iterate the loop.
i += 1;
if i >= N {
break;
}
}
println!(