Compare commits

..

10 Commits

Author SHA1 Message Date
113e9cf107 Solution for 2022, day 22, part one 2024-09-22 14:57:31 +02:00
da2bde0fdf Add solution for 2022, day 21, part two 2024-09-21 17:54:30 +02:00
ed9e9a113d Add solution for 2022, day 21, part one 2024-09-21 16:17:03 +02:00
2707271306 Update default dependencies in aoc_template
Now includes `rayon`, `itertools` and `id_arena`, as these crates
have proven very useful when writing any kind of Rust.
2024-09-21 16:15:58 +02:00
b9e2079de2 Add solutions for day20, parts one and two 2024-09-21 13:43:23 +02:00
19d5613bd9 Combined solution for parts 1+2 of 2022/day19, both finishing in <2 sec 2024-04-10 10:51:19 +02:00
aadb672532 Touches on 2022/day19-part1 2024-04-10 09:56:02 +02:00
0e1053ec5e Vastly improved solution for 2022/day19-part1 2024-04-10 09:43:37 +02:00
ede5141783 Change integer sizes to significantly reduce memory footprint 2024-04-10 07:39:06 +02:00
695d262a58 Optimized 2022/day19-part1, with a lot of potential still left
I wonder if the code I added actually improves or worsens performance.
It doesn't feel like I'm making big progress, despite all the time
spent.
2024-04-09 15:11:45 +02:00
14 changed files with 1323 additions and 285 deletions

View File

@ -6,3 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }

View File

@ -1,284 +0,0 @@
use std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
};
#[derive(Debug)]
struct Blueprint {
// The id and costs parsed from the input.
id: usize,
ore_robot_ore_cost: usize,
clay_robot_ore_cost: usize,
obsidian_robot_ore_cost: usize,
obsidian_robot_clay_cost: usize,
geode_robot_ore_cost: usize,
geode_robot_obsidian_cost: usize,
// The maximal number of geodes that can be collected by this blueprint.
// Initialized to 0 and overwritten by the solver, once it concludes.
optimal_geode_count: usize,
}
impl Blueprint {
// Solve the given blueprint.
fn solve(&mut self) {
// For performance reasons we will search the solution space using breadth-first search.
// The HashSet for the next timeslot built in the current loop iteration.
let mut next_hs: HashSet<RecursionState> = HashSet::new();
// The HashSet *of* the current timeslot that is being processed.
let mut hs: HashSet<RecursionState>;
// Initialize the simulation with the starting state.
next_hs.insert(RecursionState {
ore_robots: 1,
clay_robots: 0,
obsidian_robots: 0,
geode_robots: 0,
ore: 0,
clay: 0,
obsidian: 0,
geode: 0,
remaining_time: 24,
});
// Iterate over all timeslots.
for ts in (1usize..=24).rev() {
// println!("Now at {} remaining time. Processing {} input RSs ...", ts, next_hs.len());
// Make the new HashSet the current one and
// initialize a new one for this loop iteration.
hs = next_hs;
next_hs = HashSet::new();
let mut insert_count = 0usize;
// Process every RS of the past timeslot
// to find all the states for the current timeslot.
for rs in &hs {
// Go through all five options and branch down them, if possible.
// Specifically, we can either:
// -> Build one of the four robot types, if resources permit.
// -> Don't build anything at all.
// It's important to note that the optimal solution may include waiting in the middle,
// i.e. letting resources accumulate so we can build one of the more expensive robots
// down the line instead of immediately spending the resources on a cheaper robot type.
// In branches (1) through (4) we copy the state, commission the robot, let time pass,
// actually add the robot, and add the resulting state to the queue.
// (1) Ore Robot
if rs.ore >= self.ore_robot_ore_cost {
let mut nrs = *rs;
nrs.ore -= self.ore_robot_ore_cost;
nrs.pass_time();
nrs.ore_robots += 1;
next_hs.insert(nrs);
insert_count += 1;
// checked_insert(&mut q, nrs);
}
// (2) Clay Robot
if rs.ore >= self.clay_robot_ore_cost {
let mut nrs = *rs;
nrs.ore -= self.clay_robot_ore_cost;
nrs.pass_time();
nrs.clay_robots += 1;
next_hs.insert(nrs);
insert_count += 1;
// checked_insert(&mut q, nrs);
}
// (3) Obsidian Robot
if rs.ore >= self.obsidian_robot_ore_cost
&& rs.clay >= self.obsidian_robot_clay_cost
{
let mut nrs = *rs;
nrs.ore -= self.obsidian_robot_ore_cost;
nrs.clay -= self.obsidian_robot_clay_cost;
nrs.pass_time();
nrs.obsidian_robots += 1;
next_hs.insert(nrs);
insert_count += 1;
// checked_insert(&mut q, nrs);
}
// (4) Geode Robot
if rs.ore >= self.geode_robot_ore_cost
&& rs.obsidian >= self.geode_robot_obsidian_cost
{
let mut nrs = *rs;
nrs.ore -= self.geode_robot_ore_cost;
nrs.obsidian -= self.geode_robot_obsidian_cost;
nrs.pass_time();
nrs.geode_robots += 1;
next_hs.insert(nrs);
insert_count += 1;
// checked_insert(&mut q, nrs);
}
// (5) Build nothing and let time pass.
let mut nrs = *rs;
nrs.pass_time();
next_hs.insert(nrs);
insert_count += 1;
// checked_insert(&mut q, nrs);
}
// Done!
println!("Finished simulation round for t = {}", ts);
println!(" inserted elements: {}", insert_count);
println!(" initial next_hs.len(): {}", next_hs.len());
if next_hs.len() < 50_000 && ts > 4 {
// Next up: Reduce the number of elements in the set using the Pareto optimality comparsion.
println!("Removing redundant elements ...");
let mut reduced_hs: HashSet<RecursionState> = HashSet::new();
for rs in &next_hs {
// Carry over rs if it is part of the HS's Pareto frontier.
if next_hs.iter().all(|e| rs.compare(e).is_ge()) {
reduced_hs.insert(*rs);
}
}
next_hs = reduced_hs;
println!(" done!");
} else {
println!("Number of states too large or timeslot too late. Not optimizing.");
}
println!(" final next_hs.len(): {}\n", next_hs.len());
}
// Only RSs with no simulation-time should be left by now.
assert!(next_hs.iter().all(|e| e.remaining_time == 0));
// Print and store the result.
self.optimal_geode_count = next_hs.iter().map(|e| e.geode).max().unwrap();
println!("Found optimal geode count: {}", self.optimal_geode_count);
}
}
// Store all of the state that's passed up and down the recursion in one struct.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct RecursionState {
// The currently active fleet of robots.
ore_robots: usize,
clay_robots: usize,
obsidian_robots: usize,
geode_robots: usize,
// Our resources.
ore: usize,
clay: usize,
obsidian: usize,
geode: usize,
// How much time is left in the simulation.
remaining_time: usize,
}
impl RecursionState {
// Let one unit of time pass on this RecursionState.
// Collects all the resources from the currently active robots.
fn pass_time(&mut self) {
self.ore += self.ore_robots;
self.clay += self.clay_robots;
self.obsidian += self.obsidian_robots;
self.geode += self.geode_robots;
// One unit of time passes.
self.remaining_time -= 1;
}
// Compare two recursion states.
// This method essentially checks for Pareto improvements. An example:
// RS1: 2 ore, 2 clay, 1 ore robot, 1 clay robot, 20 minutes left
// RS2: 2 ore, 1 clay, 1 ore robot, 1 clay robot, 20 minutes left
// RS1 is just as "good" in terms of ore, robot counts and time left
// but is "strictly better" in terms of clay. It makes no sense to continue
// running the simulation for RS2 b/c it cannot possibly produce a better
// outcome than RS1.
// Having more resources, robots or time can only ever lead to better outcomes.
// If, however, RS1 had, say, one more ore but less clay than RS2 we cannot say
// that RS1 is "strictly better". It is different, having made a different tradeoff in
// resource collection, which may or may not lead to a better outcome overall.
// This comparison is used to cut off redundant simulation paths in the recursive solver.
fn compare(&self, other: &Self) -> Ordering {
// Collect the comparison result on all metrics.
// let mut metrics: Vec<Ordering> = Vec::with_capacity(9);
let mut metrics: [Ordering; 9] = [Ordering::Equal; 9];
// Resource counts
metrics[0] = self.ore.cmp(&other.ore);
metrics[1] = self.clay.cmp(&other.clay);
metrics[2] = self.obsidian.cmp(&other.obsidian);
metrics[3] = self.geode.cmp(&other.geode);
// Robot counts
metrics[4] = self.ore_robots.cmp(&other.ore_robots);
metrics[5] = self.clay_robots.cmp(&other.clay_robots);
metrics[6] = self.obsidian_robots.cmp(&other.obsidian_robots);
metrics[7] = self.geode_robots.cmp(&other.geode_robots);
// Remaining time
metrics[8] = self.remaining_time.cmp(&other.remaining_time);
// If one metric is strictly superior and the rest are better or equal,
// this RecursionState is "strictly better".
if metrics.iter().all(|e| e.is_ge()) && metrics.iter().any(|e| e.is_gt()) {
Ordering::Greater
// If one metric is strictly inferior and the rest are inferior or equal,
// this RecursionState is "strictly inferior".
} else if metrics.iter().all(|e| e.is_le()) && metrics.iter().any(|e| e.is_lt()) {
Ordering::Less
// In all other cases no definitive statement can be made.
} else {
Ordering::Equal
}
}
fn print(&self) {
print!("{:>3} O, ", self.ore);
print!("{:>3} C, ", self.clay);
print!("{:>3} B, ", self.obsidian);
print!("{:>3} G, ", self.geode);
print!("{:>3} OR, ", self.ore_robots);
print!("{:>3} CR, ", self.clay_robots);
print!("{:>3} BR, ", self.obsidian_robots);
print!("{:>3} GR, ", self.geode_robots);
println!("{:>3} T", self.remaining_time);
}
}
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");
// First, parse all the blueprints.
let mut blueprints = input
.lines()
.map(|l| l.split_whitespace().collect::<Vec<_>>())
.collect::<Vec<_>>()
.iter()
.map(|l| Blueprint {
id: l[1].trim_end_matches(':').parse::<usize>().unwrap(),
ore_robot_ore_cost: l[6].parse::<usize>().unwrap(),
clay_robot_ore_cost: l[12].parse::<usize>().unwrap(),
obsidian_robot_ore_cost: l[18].parse::<usize>().unwrap(),
obsidian_robot_clay_cost: l[21].parse::<usize>().unwrap(),
geode_robot_ore_cost: l[27].parse::<usize>().unwrap(),
geode_robot_obsidian_cost: l[30].parse::<usize>().unwrap(),
optimal_geode_count: 0,
})
.collect::<Vec<_>>();
// Solve for every blueprint.
for bp in &mut blueprints {
// Solve every blueprint with 24 minutes of time.
println!("Solving Blueprint {}", bp.id);
bp.solve();
}
println!(
"Total Quality Level: {}",
blueprints
.iter()
.map(|b| b.id * b.optimal_geode_count)
.sum::<usize>()
);
}

View File

@ -1,5 +1,5 @@
[package]
name = "day19-part1"
name = "day19"
version = "0.1.0"
edition = "2021"

357
2022/day19/src/main.rs Normal file
View File

@ -0,0 +1,357 @@
#[derive(Debug)]
struct Blueprint {
// The id and costs parsed from the input.
id: u16,
ore_robot_ore_cost: u16,
clay_robot_ore_cost: u16,
obsidian_robot_ore_cost: u16,
obsidian_robot_clay_cost: u16,
geode_robot_ore_cost: u16,
geode_robot_obsidian_cost: u16,
// The maximal number of geodes that can be collected by this blueprint.
// Initialized to 0 and overwritten by the solver, once it concludes.
optimal_geode_count: u16,
}
impl Blueprint {
// Solve the given blueprint using BFS.
fn solve_bfs(&mut self, total_runtime: u16) {
// For performance reasons we will search the solution space using breadth-first search.
// vec_a has the RecursionStates for the current timeslot, while vec_b has the next slot's states.
let mut vec_a: Vec<RecursionState> = Vec::with_capacity(2u64.pow(20) as usize);
let mut vec_b: Vec<RecursionState> = Vec::with_capacity(2u64.pow(20) as usize);
// Initialize the simulation with the starting state.
vec_a.push(RecursionState {
ore_robots: 1,
clay_robots: 0,
obsidian_robots: 0,
geode_robots: 0,
ore: 0,
clay: 0,
obsidian: 0,
geode: 0,
});
// Iterate over all timeslots.
// Building a robot at t=1 cannot influence the final geode-count,
// so it's omitted from the simulation here.
let mut early_exit = false;
for ts in (2u16..=total_runtime).rev() {
// Have we reached >= 2^20 elements on the input? Time to go for DFS instead.
// Additionally, the queue-overhead shouldn't be worth it for the last few timesteps.
if vec_a.len() >= 2u64.pow(20) as usize || ts <= 3 {
// println!("Switching to recursive solving ...");
// Iterate over all possibilities and run recursively.
for rs in &vec_a {
self.solve_recursive(*rs, ts);
}
// And now we're done proper, no need to run the remaining loop iterations.
early_exit = true;
break;
}
// Process every RS of the past timeslot
// to find all the states for the current timeslot.
for rs in &vec_a {
// Go through all five options and branch down them, if possible.
// Specifically, we can either:
// -> Build one of the four robot types, if resources permit.
// -> Don't build anything at all.
// It's important to note that the optimal solution may include waiting in the middle,
// i.e. letting resources accumulate so we can build one of the more expensive robots
// down the line instead of immediately spending the resources on a cheaper robot type.
// Copy over the current state and let time for it pass.
// This is the same no matter what type of robot we build since the robot will
// go live at the end of the timeslot, not at its beginning.
let mut next_rs = *rs;
next_rs.ore += next_rs.ore_robots as u16;
next_rs.clay += next_rs.clay_robots as u16;
next_rs.obsidian += next_rs.obsidian_robots as u16;
next_rs.geode += next_rs.geode_robots as u16;
// Check whether we can build the different robots, using `rs` and not `next_rs`
// since the resources have to be allocated at the beginning of the turn.
// (1) Ore Robot
if rs.ore >= self.ore_robot_ore_cost {
let mut nrs = next_rs;
nrs.ore -= self.ore_robot_ore_cost;
nrs.ore_robots += 1;
vec_b.push(nrs);
}
// (2) Clay Robot
if rs.ore >= self.clay_robot_ore_cost {
let mut nrs = next_rs;
nrs.ore -= self.clay_robot_ore_cost;
nrs.clay_robots += 1;
vec_b.push(nrs);
}
// (3) Obsidian Robot
if rs.ore >= self.obsidian_robot_ore_cost
&& rs.clay >= self.obsidian_robot_clay_cost
{
let mut nrs = next_rs;
nrs.ore -= self.obsidian_robot_ore_cost;
nrs.clay -= self.obsidian_robot_clay_cost;
nrs.obsidian_robots += 1;
vec_b.push(nrs);
}
// (4) Geode Robot
if rs.ore >= self.geode_robot_ore_cost
&& rs.obsidian >= self.geode_robot_obsidian_cost
{
let mut nrs = next_rs;
nrs.ore -= self.geode_robot_ore_cost;
nrs.obsidian -= self.geode_robot_obsidian_cost;
nrs.geode_robots += 1;
vec_b.push(nrs);
}
// (5) Build nothing and let time pass.
vec_b.push(next_rs);
}
// Done!
// println!("Finished simulation round for t = {}", ts);
// println!(" inserted elements: {}", vec_b.len());
// Prune elements.
prune_states(&mut vec_b, &mut vec_a);
// println!(" elements after prune: {}", vec_a.len());
// Clear vec_b since all the relevant states have been copied over to vec_a.
vec_b.clear();
}
// Collect and print the final geode count.
// Remember that we still have to simulate the geode-collection for t=1,
// hence `e.geode + e.geode_robots as u16`.
if !early_exit {
self.optimal_geode_count = vec_a
.iter()
.map(|e| e.geode + e.geode_robots as u16)
.max()
.unwrap();
}
// println!("Found optimal geode count: {}", self.optimal_geode_count);
}
// Solve the task recursively, providing the current state and remaining time.
// Essentially, and in contrast to `solve_bfs`, this recursive solver performs
// depth-first-search (DFS) on the solution space instead of BFS.
// This removes our ability to prune redundant elements, but doesn't require
// keeping a queue of elements, making for a *much* lighter memory footprint.
// Recommended for the final few timesteps.
fn solve_recursive(&mut self, rs: RecursionState, t: u16) {
// print!("t = {}, ", t);
// rs.print();
// println!();
// Exit condition. If t == 1, we're basically done.
// No need to build the final robot, it can't influence the final geode result.
// Simply add one more round of harvesting (rs.geode_robots) and check for improvements.
if t == 1 {
let next_geode_count = rs.geode + rs.geode_robots as u16;
if next_geode_count > self.optimal_geode_count {
// Update the optimal result, if improved.
self.optimal_geode_count = next_geode_count;
}
return;
}
// Check a cutoff-condition, in case this branch is not worth it.
let upper_bound = rs.geode // The resources we already have.
// The resource the already existing robots would produce.
+ rs.geode_robots as u16 * t
// The resources we would get if we produced one robot every timeslot.
// This is the triangular number for (t - 1).
+ (t - 1) * t / 2;
// Now check if this would be an improvement.
if upper_bound <= self.optimal_geode_count {
// No point continuing.
return;
}
// The following section is basically the same as in `solve_bfs`.
// Copy over the current state and let time for it pass.
// This is the same no matter what type of robot we build since the robot will
// go live at the end of the timeslot, not at its beginning.
let mut next_rs = rs;
next_rs.ore += next_rs.ore_robots as u16;
next_rs.clay += next_rs.clay_robots as u16;
next_rs.obsidian += next_rs.obsidian_robots as u16;
next_rs.geode += next_rs.geode_robots as u16;
// Check whether we can build the different robots, using `rs` and not `next_rs`
// since the resources have to be allocated at the beginning of the turn.
// (1) Ore Robot
if rs.ore >= self.ore_robot_ore_cost {
let mut nrs = next_rs;
nrs.ore -= self.ore_robot_ore_cost;
nrs.ore_robots += 1;
self.solve_recursive(nrs, t - 1);
}
// (2) Clay Robot
if rs.ore >= self.clay_robot_ore_cost {
let mut nrs = next_rs;
nrs.ore -= self.clay_robot_ore_cost;
nrs.clay_robots += 1;
self.solve_recursive(nrs, t - 1);
}
// (3) Obsidian Robot
if rs.ore >= self.obsidian_robot_ore_cost && rs.clay >= self.obsidian_robot_clay_cost {
let mut nrs = next_rs;
nrs.ore -= self.obsidian_robot_ore_cost;
nrs.clay -= self.obsidian_robot_clay_cost;
nrs.obsidian_robots += 1;
self.solve_recursive(nrs, t - 1);
}
// (4) Geode Robot
if rs.ore >= self.geode_robot_ore_cost && rs.obsidian >= self.geode_robot_obsidian_cost {
let mut nrs = next_rs;
nrs.ore -= self.geode_robot_ore_cost;
nrs.obsidian -= self.geode_robot_obsidian_cost;
nrs.geode_robots += 1;
self.solve_recursive(nrs, t - 1);
}
// (5) Build nothing and let time pass.
self.solve_recursive(next_rs, t - 1);
}
}
// Store all of the state that's passed up and down the recursion in one struct.
// Derive PartialOrd + Ord for lexicographic sorting, something we'll use during pruning.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct RecursionState {
// The currently active fleet of robots.
ore_robots: u8,
clay_robots: u8,
obsidian_robots: u8,
geode_robots: u8,
// Our resources.
ore: u16,
clay: u16,
obsidian: u16,
geode: u16,
}
impl RecursionState {
#[allow(dead_code)]
fn print(&self) {
print!("{:>3} OR, ", self.ore_robots);
print!("{:>3} CR, ", self.clay_robots);
print!("{:>3} BR, ", self.obsidian_robots);
print!("{:>3} GR, ", self.geode_robots);
print!("{:>3} O, ", self.ore);
print!("{:>3} C, ", self.clay);
print!("{:>3} B, ", self.obsidian);
print!("{:>3} G, ", self.geode);
}
}
// Copy over states for the next simulation round, pruning a lot of (but not all)
// RecursionStates that are "strictly inferior" in terms of Pareto optimality.
// Will clear any previously present elements in `dest`.
//
// A few words on the general idea here:
// The goal here is to check for Pareto improvements. An example:
// RS1: 2 ore, 2 clay, 1 ore robot, 1 clay robot, 20 minutes left
// RS2: 2 ore, 1 clay, 1 ore robot, 1 clay robot, 20 minutes left
// RS1 is just as "good" in terms of ore, robot counts and time left
// but is "strictly better" in terms of clay. It makes no sense to continue
// running the simulation for RS2 b/c it cannot possibly produce a better
// outcome than RS1.
// Having more resources, robots or time can only ever lead to better outcomes.
// If, however, RS1 had, say, one more ore but less clay than RS2 we cannot say
// that RS1 is "strictly better". It is different, having made a different tradeoff in
// resource collection, which may or may not lead to a better outcome overall.
// This comparison is used to cut off redundant simulation paths in the solver.
fn prune_states(source: &mut [RecursionState], dest: &mut Vec<RecursionState>) {
// Begin by sorting the source lexicrgraphically and clearing the destination.
source.sort_unstable();
dest.clear();
// Iterate through it from smallest to largest element and look at every pair of states.
for (a, b) in source.iter().zip(source.iter().skip(1)) {
// Don't copy a over if it is strictly inferior or equal to b.
// Compare from bottom to top.
if a.geode > b.geode
|| a.obsidian > b.obsidian
|| a.clay > b.clay
|| a.ore > b.ore
|| a.geode_robots > b.geode_robots
|| a.obsidian_robots > b.obsidian_robots
|| a.clay_robots > b.clay_robots
|| a.ore_robots > b.ore_robots
{
dest.push(*a);
}
}
// Copy over the very last element, too.
dest.push(*source.last().unwrap());
}
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");
// First, parse all the blueprints.
let mut blueprints = input
.lines()
.map(|l| l.split_whitespace().collect::<Vec<_>>())
.collect::<Vec<_>>()
.iter()
.map(|l| Blueprint {
id: l[1].trim_end_matches(':').parse::<u16>().unwrap(),
ore_robot_ore_cost: l[6].parse::<u16>().unwrap(),
clay_robot_ore_cost: l[12].parse::<u16>().unwrap(),
obsidian_robot_ore_cost: l[18].parse::<u16>().unwrap(),
obsidian_robot_clay_cost: l[21].parse::<u16>().unwrap(),
geode_robot_ore_cost: l[27].parse::<u16>().unwrap(),
geode_robot_obsidian_cost: l[30].parse::<u16>().unwrap(),
optimal_geode_count: 0,
})
.collect::<Vec<_>>();
// PART ONE
// Solve for every blueprint with time 24.
for bp in &mut blueprints {
// Solve every blueprint with TOTAL_RUNTIME minutes of time.
// println!("Solving Blueprint {}", bp.id);
bp.solve_bfs(24u16);
}
println!(
"Total Quality Level for Part 1: {}",
blueprints
.iter()
.map(|b| b.id * b.optimal_geode_count)
.sum::<u16>()
);
// PART TWO
// Now solve the first three blueprints again, but for 32 minutes.
for bp in blueprints.iter_mut().take(3) {
bp.solve_bfs(32u16);
}
println!(
"Multiplied Geode Counts for Part 2: {}",
blueprints
.iter()
.take(3)
.map(|b| b.optimal_geode_count as u64)
.product::<u64>()
);
}

View File

@ -0,0 +1,11 @@
[package]
name = "day20-part1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }

View File

@ -0,0 +1,146 @@
use id_arena::{Arena, Id};
use itertools::Itertools;
type NodeId = Id<Node>;
#[derive(Debug, Eq, PartialEq)]
struct Node {
value: i32,
prev: Option<NodeId>,
next: Option<NodeId>,
}
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");
// The actual set of initial numbers, completely immutable.
let numbers = input
.lines()
.map(|e| e.parse::<i32>().unwrap())
.collect_vec();
// Translate this list into a self-rolled linked-list with the help of the `id-arena` crate.
let mut nodes = Arena::<Node>::with_capacity(numbers.len());
// Keep the original order of references for the mixing operation.
let mut og_order: Vec<NodeId> = Vec::with_capacity(numbers.len());
// Fill the arena with the Nodes.
let mut prev_node = None;
for num in numbers {
// Create the current Node.
let current_node = nodes.alloc(Node {
value: num,
prev: prev_node,
next: None,
});
og_order.push(current_node);
// Fix the next-pointer of the previous node, if it exists.
if let Some(pn) = prev_node {
nodes[pn].next = Some(current_node);
}
// And make the current element the previous one.
prev_node = Some(current_node);
}
// Make the linked list cyclic, as that matches the mixing behvaior of the task.
let first = og_order.first().copied().unwrap();
let last = og_order.last().copied().unwrap();
nodes[first].prev = Some(last);
nodes[last].next = Some(first);
// We'll need it later: Find the element with value 0, it has special significance.
let zero_node = nodes
.iter()
.find_map(|(i, e)| if e.value == 0 { Some(i) } else { None })
.unwrap();
let print_list = |a: &Arena<Node>, s: NodeId| {
print!("[");
let mut c = s;
loop {
print!("{}", a[c].value);
c = a[c].next.unwrap();
if c == s {
break;
}
print!(", ");
}
println!("]");
};
// Now iterate through all nodes in their original order.
for id in og_order {
// print_list(&nodes, zero_node);
// Save the hassle if the value is 0.
if nodes[id].value == 0 {
continue;
}
// Keep track of the element after which to insert the current one.
let mut target = nodes[id].prev.unwrap();
// Move as many steps as the value requires.
for _ in 0..nodes[id].value.abs() {
// Move left or right, depending on sign of value.
if nodes[id].value.is_negative() {
// println!("Moving {} to the left.", nodes[id].value);
target = nodes[target].prev.unwrap();
// Move again if the target is ourselves.
if target == id {
// println!("Skipping self, moving to the left.");
target = nodes[target].prev.unwrap();
}
} else {
// println!("Moving {} to the right.", nodes[id].value);
target = nodes[target].next.unwrap();
// Move again if the target is ourselves.
if target == id {
// println!("Skipping self, moving to the right.");
target = nodes[target].next.unwrap();
}
}
}
// Actually perform the swap.
let new_prev = target;
let new_next = nodes[target].next.unwrap();
let old_prev = nodes[id].prev.unwrap();
let old_next = nodes[id].next.unwrap();
// Rebind the old neighbors to each other.
nodes[old_prev].next = Some(old_next);
nodes[old_next].prev = Some(old_prev);
// Bind the new neighbors to the node.
nodes[new_prev].next = Some(id);
nodes[new_next].prev = Some(id);
// Bind the node itself to the new neighbors.
nodes[id].prev = Some(new_prev);
nodes[id].next = Some(new_next);
}
// print_list(&nodes, zero_node);
// Find the 1000th, 2000th and 3000th element, starting form the zero-element,
// and add them together for the final result.
let mut result = 0i32;
let mut current_node = zero_node;
for i in 1..=3000 {
current_node = nodes[current_node].next.unwrap();
if i % 1000 == 0 {
// println!("Adding {}.", nodes[current_node].value);
result += nodes[current_node].value;
}
}
println!("Result: {}", result);
}

View File

@ -0,0 +1,11 @@
[package]
name = "day20-part2"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }

View File

@ -0,0 +1,158 @@
use id_arena::{Arena, Id};
use itertools::Itertools;
type NodeId = Id<Node>;
#[derive(Debug, Eq, PartialEq)]
struct Node {
value: i64,
prev: Option<NodeId>,
next: Option<NodeId>,
}
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");
// The actual set of initial numbers, completely immutable.
// Here the "decryption key" is multiplied into the list.
let numbers = input
.lines()
// .map(|e| e.parse::<i64>().unwrap())
.map(|e| e.parse::<i64>().unwrap() * 811589153)
.collect_vec();
// Translate this list into a self-rolled linked-list with the help of the `id-arena` crate.
let mut nodes = Arena::<Node>::with_capacity(numbers.len());
// Keep the original order of references for the mixing operation.
let mut og_order: Vec<NodeId> = Vec::with_capacity(numbers.len());
// Fill the arena with the Nodes.
let mut prev_node = None;
for num in numbers {
// Create the current Node.
let current_node = nodes.alloc(Node {
value: num,
prev: prev_node,
next: None,
});
og_order.push(current_node);
// Fix the next-pointer of the previous node, if it exists.
if let Some(pn) = prev_node {
nodes[pn].next = Some(current_node);
}
// And make the current element the previous one.
prev_node = Some(current_node);
}
// Make the linked list cyclic, as that matches the mixing behvaior of the task.
let first = og_order.first().copied().unwrap();
let last = og_order.last().copied().unwrap();
nodes[first].prev = Some(last);
nodes[last].next = Some(first);
// We'll need it later: Find the element with value 0, it has special significance.
let zero_node = nodes
.iter()
.find_map(|(i, e)| if e.value == 0 { Some(i) } else { None })
.unwrap();
let print_list = |a: &Arena<Node>, s: NodeId| {
print!("[");
let mut c = s;
loop {
print!("{}", a[c].value);
c = a[c].next.unwrap();
if c == s {
break;
}
print!(", ");
}
println!("]");
};
// Now iterate through all nodes in their original order - ten times (for some reason).
for (i, id) in (0..10).cartesian_product(og_order.iter().copied()) {
// print!("Iteration {} / ", i);
// print_list(&nodes, zero_node);
// Save the hassle if the value is 0.
if nodes[id].value == 0 {
continue;
}
let move_value = nodes[id].value;
let mut move_amount = move_value.abs();
let move_reverse = move_value.is_negative();
// Automatically wrap moves larger than the entire list.
// Subtract one since the list over which we move does not contain the element we're
// moving - conceptually it's a list with 4999 elements, not 5000.
move_amount %= (nodes.len() as i64) - 1;
// Keep track of the element after which to insert the current one.
let mut target = nodes[id].prev.unwrap();
// Move as many steps as the value requires.
for _ in 0..move_amount {
// Move left or right, depending on sign of value.
if move_reverse {
// println!("Moving {} to the left.", nodes[id].value);
target = nodes[target].prev.unwrap();
// Move again if the target is ourselves.
if target == id {
// println!("Skipping self, moving to the left.");
target = nodes[target].prev.unwrap();
}
} else {
// println!("Moving {} to the right.", nodes[id].value);
target = nodes[target].next.unwrap();
// Move again if the target is ourselves.
if target == id {
// println!("Skipping self, moving to the right.");
target = nodes[target].next.unwrap();
}
}
}
// Actually perform the swap.
let new_prev = target;
let new_next = nodes[target].next.unwrap();
let old_prev = nodes[id].prev.unwrap();
let old_next = nodes[id].next.unwrap();
// Rebind the old neighbors to each other.
nodes[old_prev].next = Some(old_next);
nodes[old_next].prev = Some(old_prev);
// Bind the new neighbors to the node.
nodes[new_prev].next = Some(id);
nodes[new_next].prev = Some(id);
// Bind the node itself to the new neighbors.
nodes[id].prev = Some(new_prev);
nodes[id].next = Some(new_next);
}
// print_list(&nodes, zero_node);
// Find the 1000th, 2000th and 3000th element, starting form the zero-element,
// and add them together for the final result.
let mut result = 0i64;
let mut current_node = zero_node;
for i in 1..=3000 {
current_node = nodes[current_node].next.unwrap();
if i % 1000 == 0 {
// println!("Adding {}.", nodes[current_node].value);
result += nodes[current_node].value;
}
}
println!("Result: {}", result);
}

View File

@ -0,0 +1,11 @@
[package]
name = "day21-part1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }

View File

@ -0,0 +1,107 @@
use itertools::Itertools;
use std::collections::HashMap;
use id_arena::{Arena, Id};
type MonkeyNumber = i64;
type NodeId = Id<Node>;
#[derive(Debug)]
enum Node {
Uninitialized,
Unit(MonkeyNumber),
Add(NodeId, NodeId),
Sub(NodeId, NodeId),
Mul(NodeId, NodeId),
Div(NodeId, NodeId),
}
impl Node {
/// Evaluates the expression tree with the given Node as root node and returns the calculated result.
///
/// Uses recursion under-the-hood to implement the evaluation.
/// The recursive strategy is essentially a depth-first-search algorithm.
fn evaluate(&self, nodes: &Arena<Node>) -> MonkeyNumber {
match self {
Node::Unit(value) => *value,
Node::Add(lhs, rhs) => nodes[*lhs].evaluate(nodes) + nodes[*rhs].evaluate(nodes),
Node::Sub(lhs, rhs) => nodes[*lhs].evaluate(nodes) - nodes[*rhs].evaluate(nodes),
Node::Mul(lhs, rhs) => nodes[*lhs].evaluate(nodes) * nodes[*rhs].evaluate(nodes),
Node::Div(lhs, rhs) => nodes[*lhs].evaluate(nodes) / nodes[*rhs].evaluate(nodes),
Node::Uninitialized => panic!("encountered uninitialized Node during evaluation"),
}
}
}
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");
// --- TASK BEGIN ---
// The complete collection of operation nodes.
let mut nodes = Arena::<Node>::with_capacity(input.lines().count());
// Dictionary mapping the 4-letter identifiers to their respective NodeId.
let mut identifiers = HashMap::<[u8; 4], NodeId>::new();
// First, create unitinitialized nodes and populate the identifier-dictionary.
for line in input.lines() {
let node = nodes.alloc(Node::Uninitialized);
identifiers.insert(line[0..4].as_bytes().try_into().unwrap(), node);
}
// Then, actually populate the nodes.
for line in input.lines() {
// Split into node_id and expression.
let (node_id, expr) = line.split(": ").collect_tuple().unwrap();
// Convert node_id from `&str` to `&[u8; 4]`.
let node_id: &[u8; 4] = node_id.as_bytes().try_into().unwrap();
// Grab the actual NodeId with the identifier.
let node = identifiers[node_id];
// Try to see if we can parse
// -> an operator character (+, -, *, /)
// -> a left- and right-hand-side of the operation
// -> an integer for the whole expression
let op_char = expr.chars().nth(5);
let lhs: Option<&[u8; 4]> = expr
.as_bytes()
.get(0..4)
.map(|e| e.try_into().ok())
.flatten();
let rhs: Option<&[u8; 4]> = expr
.as_bytes()
.get(7..11)
.map(|e| e.try_into().ok())
.flatten();
let monkey_number = expr.parse::<MonkeyNumber>().ok();
// Now actually assign the node, depending on what we could and could not match.
nodes[node] = match (op_char, lhs, rhs, monkey_number) {
(Some('+'), Some(lhs), Some(rhs), None) => {
Node::Add(identifiers[lhs], identifiers[rhs])
}
(Some('-'), Some(lhs), Some(rhs), None) => {
Node::Sub(identifiers[lhs], identifiers[rhs])
}
(Some('*'), Some(lhs), Some(rhs), None) => {
Node::Mul(identifiers[lhs], identifiers[rhs])
}
(Some('/'), Some(lhs), Some(rhs), None) => {
Node::Div(identifiers[lhs], identifiers[rhs])
}
(_, _, _, Some(monkey_number)) => Node::Unit(monkey_number),
_ => panic!("could not parse line in input"),
};
}
// Evaluate the expression tree and print the result.
println!("Result: {}", nodes[identifiers[b"root"]].evaluate(&nodes));
}

View File

@ -0,0 +1,11 @@
[package]
name = "day21-part2"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }

View File

@ -0,0 +1,236 @@
use itertools::Itertools;
use std::collections::HashMap;
use id_arena::{Arena, Id};
type MonkeyNumber = i64;
type NodeId = Id<Node>;
#[derive(Debug, Clone)]
enum Node {
Uninitialized,
Unit(MonkeyNumber),
Add(NodeId, NodeId),
Sub(NodeId, NodeId),
Mul(NodeId, NodeId),
Div(NodeId, NodeId),
Human { multiplier: f64, addend: f64 },
}
/// Reduces the expression tree to a minimal representation containing the human value, a.k.a. `x`.
///
/// This modifies the tree. After a reduce operation, each Subtree reduces either
/// into a Unit-type or a Human-type. Crucially, each subtree becomes a leaf.
fn reduce_tree(tree: &mut Arena<Node>, node: NodeId) {
match tree[node].clone() {
// Uninitialized Nodes are illegal at this point.
Node::Uninitialized => panic!("encountered uninitialized Node during evaluation"),
// There is nothing to reduce if this node is already a Unit or Human.
Node::Unit(..) | Node::Human { .. } => (),
Node::Add(lhs, rhs) | Node::Sub(lhs, rhs) | Node::Mul(lhs, rhs) | Node::Div(lhs, rhs) => {
// First, reduce the subtrees.
reduce_tree(tree, lhs);
reduce_tree(tree, rhs);
// Next, merge them depending on subtypes.
match (tree[node].clone(), tree[lhs].clone(), tree[rhs].clone()) {
(Node::Add(..), Node::Unit(lv), Node::Unit(rv)) => tree[node] = Node::Unit(lv + rv),
(Node::Sub(..), Node::Unit(lv), Node::Unit(rv)) => tree[node] = Node::Unit(lv - rv),
(Node::Mul(..), Node::Unit(lv), Node::Unit(rv)) => tree[node] = Node::Unit(lv * rv),
(Node::Div(..), Node::Unit(lv), Node::Unit(rv)) => tree[node] = Node::Unit(lv / rv),
(Node::Add(..), Node::Human { multiplier, addend }, Node::Unit(rv)) => {
tree[node] = Node::Human {
multiplier,
addend: addend + rv as f64,
};
}
(Node::Sub(..), Node::Human { multiplier, addend }, Node::Unit(rv)) => {
tree[node] = Node::Human {
multiplier,
addend: addend - rv as f64,
};
}
(Node::Mul(..), Node::Human { multiplier, addend }, Node::Unit(rv)) => {
tree[node] = Node::Human {
multiplier: multiplier * rv as f64,
addend: addend * rv as f64,
};
}
(Node::Div(..), Node::Human { multiplier, addend }, Node::Unit(rv)) => {
tree[node] = Node::Human {
multiplier: multiplier / rv as f64,
addend: addend / rv as f64,
};
}
(Node::Add(..), Node::Unit(lv), Node::Human { multiplier, addend }) => {
tree[node] = Node::Human {
multiplier,
addend: lv as f64 + addend,
};
}
(Node::Sub(..), Node::Unit(lv), Node::Human { multiplier, addend }) => {
tree[node] = Node::Human {
multiplier: - multiplier,
addend: lv as f64 - addend,
};
}
(Node::Mul(..), Node::Unit(lv), Node::Human { multiplier, addend }) => {
tree[node] = Node::Human {
multiplier: lv as f64 * multiplier,
addend: lv as f64 * addend,
};
}
(Node::Div(..), Node::Unit(lv), Node::Human { multiplier, addend }) => {
tree[node] = Node::Human {
multiplier: lv as f64 / multiplier,
addend: lv as f64 / addend,
};
}
_ => panic!("illegal tree pattern encountered - cannot reduce")
}
}
}
}
fn print_tree(tree: &Arena<Node>, node: NodeId) {
match tree[node] {
Node::Unit(value) => print!("{value}"),
Node::Human { multiplier, addend } => print!("[x * {multiplier} + {addend}]"),
Node::Add(lhs, rhs) => {
print!("(");
print_tree(tree, lhs);
print!(" + ");
print_tree(tree, rhs);
print!(")");
}
Node::Sub(lhs, rhs) => {
print!("(");
print_tree(tree, lhs);
print!(" - ");
print_tree(tree, rhs);
print!(")");
}
Node::Mul(lhs, rhs) => {
print!("(");
print_tree(tree, lhs);
print!(" * ");
print_tree(tree, rhs);
print!(")");
}
Node::Div(lhs, rhs) => {
print!("(");
print_tree(tree, lhs);
print!(" / ");
print_tree(tree, rhs);
print!(")");
}
Node::Uninitialized => panic!("encountered uninitialized Node during evaluation"),
}
}
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");
// --- TASK BEGIN ---
// The complete collection of operation nodes.
let mut nodes = Arena::<Node>::with_capacity(input.lines().count());
// Dictionary mapping the 4-letter identifiers to their respective NodeId.
let mut identifiers = HashMap::<[u8; 4], NodeId>::new();
// First, create unitinitialized nodes and populate the identifier-dictionary.
for line in input.lines() {
let node = nodes.alloc(Node::Uninitialized);
identifiers.insert(line[0..4].as_bytes().try_into().unwrap(), node);
}
// Then, actually populate the nodes.
for line in input.lines() {
// Split into node_id and expression.
let (node_id, expr) = line.split(": ").collect_tuple().unwrap();
// Convert node_id from `&str` to `&[u8; 4]`.
let node_id: &[u8; 4] = node_id.as_bytes().try_into().unwrap();
// Grab the actual NodeId with the identifier.
let node = identifiers[node_id];
// Try to see if we can parse
// -> an operator character (+, -, *, /)
// -> a left- and right-hand-side of the operation
// -> an integer for the whole expression
let mut op_char = expr.chars().nth(5);
let lhs: Option<&[u8; 4]> = expr
.as_bytes()
.get(0..4)
.map(|e| e.try_into().ok())
.flatten();
let rhs: Option<&[u8; 4]> = expr
.as_bytes()
.get(7..11)
.map(|e| e.try_into().ok())
.flatten();
let monkey_number = expr.parse::<MonkeyNumber>().ok();
// If this is the 'root' node, override the operation with Sub.
// This is basically akin to an equality check,
// returning 0 if both numbers are equal, and non-zero otherwise.
if node_id == b"root" {
op_char = Some('-');
}
// If this is the human node, override the type and move on to the next.
if node_id == b"humn" {
nodes[node] = Node::Human {
multiplier: 1.0,
addend: 0.0,
};
continue;
}
// Now actually assign the node, depending on what we could and could not match.
nodes[node] = match (op_char, lhs, rhs, monkey_number) {
(Some('+'), Some(lhs), Some(rhs), None) => {
Node::Add(identifiers[lhs], identifiers[rhs])
}
(Some('-'), Some(lhs), Some(rhs), None) => {
Node::Sub(identifiers[lhs], identifiers[rhs])
}
(Some('*'), Some(lhs), Some(rhs), None) => {
Node::Mul(identifiers[lhs], identifiers[rhs])
}
(Some('/'), Some(lhs), Some(rhs), None) => {
Node::Div(identifiers[lhs], identifiers[rhs])
}
(_, _, _, Some(monkey_number)) => Node::Unit(monkey_number),
_ => panic!("could not parse line in input"),
};
}
// Locate the 'humn' and 'root' nodes.
let root_node = identifiers[b"root"];
print_tree(&nodes, root_node);
println!();
// Now reduce the tree and see what happens.
reduce_tree(&mut nodes, root_node);
print_tree(&nodes, root_node);
println!();
// We assume the tree is now one Human-node. Let's solve it for 0.
let x = if let Node::Human { multiplier, addend } = nodes[root_node] {
- addend / multiplier
} else {
panic!("tree reduction didn't yield human node")
};
println!("The x you're looking for is {}.", x);
}

View File

@ -0,0 +1,12 @@
[package]
name = "day22-part1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rayon = "1"
itertools = "0"
id-arena = { version = "2", features = ["rayon"] }
ndarray = { version = "0", features = ["rayon"] }

View File

@ -0,0 +1,259 @@
use itertools::Itertools;
use ndarray::prelude::*;
/// The state each cell in the map can be in.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum MapCell {
/// Terrain where the player can be.
Open,
/// Terrain where the player cannot be.
Wall,
/// Terrain that is not part of the map.
/// The player wraps around if they are on this tile.
Void,
}
type MonkeyMap = Array<MapCell, Dim<[usize; 2]>>;
/// A single command from the input's command-chain.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Command {
Movement(u32),
TurnRight,
TurnLeft,
}
/// The four directions the player can face.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Direction {
Right = 0,
Down,
Left,
Up,
}
impl Direction {
/// Returns the direction you'd get when turning right.
fn turn_right(&self) -> Direction {
match self {
Direction::Up => Direction::Right,
Direction::Down => Direction::Left,
Direction::Left => Direction::Up,
Direction::Right => Direction::Down,
}
}
/// Returns the direction you'd get when turning left.
fn turn_left(&self) -> Direction {
match self {
Direction::Up => Direction::Left,
Direction::Down => Direction::Right,
Direction::Left => Direction::Down,
Direction::Right => Direction::Up,
}
}
}
/// A single instance of a player existing on the map.
#[derive(Debug, Clone)]
struct Player<'a> {
x_pos: usize,
y_pos: usize,
direction: Direction,
monkey_map: &'a MonkeyMap,
}
impl Player<'_> {
/// Run the simulation for the given list of commands.
fn run_simulation(&mut self, commands: &[Command]) {
for com in commands {
// self.print_map();
match com {
Command::TurnLeft => {
self.direction = self.direction.turn_left();
}
Command::TurnRight => {
self.direction = self.direction.turn_right();
}
Command::Movement(mv) => {
for _ in 0..*mv {
self.move_one_tile();
}
}
}
}
}
/// Move the player by one tile along the direction they're facing.
fn move_one_tile(&mut self) {
let height = self.monkey_map.dim().0;
let width = self.monkey_map.dim().1;
// Keep track of our original position.
let og_x = self.x_pos;
let og_y = self.y_pos;
loop {
// Determine the theoretical new coordianates.
let (new_x, new_y) = match self.direction {
Direction::Up => (self.x_pos as i32, self.y_pos as i32 - 1),
Direction::Down => (self.x_pos as i32, self.y_pos as i32 + 1),
Direction::Left => (self.x_pos as i32 - 1, self.y_pos as i32),
Direction::Right => (self.x_pos as i32 + 1, self.y_pos as i32),
};
// Check if they're out-of-bounds and wrap them around, if so.
let new_x = if new_x < 0 {
width - 1
} else if new_x >= width as i32 {
0usize
} else {
new_x as usize
};
let new_y = if new_y < 0 {
height - 1
} else if new_y >= height as i32 {
0usize
} else {
new_y as usize
};
// Next, check the kind of tile we've reached.
match self.monkey_map[[new_y, new_x]] {
MapCell::Open => {
// Free space? Great, then move there and return.
self.x_pos = new_x;
self.y_pos = new_y;
return;
}
MapCell::Wall => {
// Wall? We can't move there.
// Return to where we started (in case we moved through the void) and return.
self.x_pos = og_x;
self.y_pos = og_y;
return;
}
MapCell::Void => {
// If we're in the Void we need to wrap around, i.e. keep moving.
// Update the position but don't return just yet.
// This way we'll run another loop iteration.
self.x_pos = new_x;
self.y_pos = new_y;
}
}
}
}
/// Calculate the final solution as outlined in the task description.
fn calculate_solution(&self) -> usize {
((self.y_pos + 1) * 1000) + ((self.x_pos + 1) * 4) + (self.direction as usize)
}
/// Print the current state of the map as well as the player's location for debugging.
#[allow(dead_code)]
fn print_map(&self) {
println!();
let height = self.monkey_map.dim().0;
let width = self.monkey_map.dim().1;
for y in 0..height {
for x in 0..width {
if x == self.x_pos && y == self.y_pos {
match self.direction {
Direction::Up => print!("^"),
Direction::Down => print!("v"),
Direction::Left => print!("<"),
Direction::Right => print!(">"),
}
} else {
match self.monkey_map[[y, x]] {
MapCell::Void => print!(" "),
MapCell::Open => print!("."),
MapCell::Wall => print!("#"),
}
}
}
println!();
}
}
}
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");
// --- TASK BEGIN ---
let mut lines = input.lines().collect_vec();
// Parse the list of commands.
let commands = lines
.last()
.unwrap()
.chars()
.chunk_by(|e| e.is_ascii_digit())
.into_iter()
.map(|(is_number, chunk)| {
let chunk = chunk.collect::<String>();
match (is_number, chunk.chars().next().unwrap()) {
(true, _) => Command::Movement(chunk.parse::<u32>().unwrap()),
(false, 'R') => Command::TurnRight,
(false, 'L') => Command::TurnLeft,
_ => panic!("could not parse series of movement commands"),
}
})
.collect_vec();
// Get rid of the last two lines, as they're the only ones not part of the map.
lines.pop();
lines.pop();
// Determine the height and width of the map.
let width = lines.iter().map(|e| e.len()).max().unwrap();
let height = lines.len();
// Allocate it using ndarray.
let mut monkey_map = Array::<MapCell, _>::from_elem((height, width), MapCell::Void);
// Now populate the map with the map-data.
for (y, line) in lines.iter().enumerate() {
for (x, c) in line.chars().enumerate() {
monkey_map[[y, x]] = match c {
' ' => MapCell::Void,
'.' => MapCell::Open,
'#' => MapCell::Wall,
_ => panic!("unkown character detected in drawing of map"),
}
}
}
// Determine the initial coordinates.
let (initial_y, initial_x) = monkey_map
.indexed_iter()
.find_map(|((y, x), c)| {
if *c != MapCell::Void {
Some((y, x))
} else {
None
}
})
.unwrap();
// Construct a player out of this.
let mut player = Player {
y_pos: initial_y,
x_pos: initial_x,
direction: Direction::Right,
monkey_map: &monkey_map,
};
// And let the player run the simulation.
player.run_simulation(&commands);
// Calculate and return the solution.
println!("Result: {}", player.calculate_solution());
}