diff --git a/2022/day16-part2/Cargo.toml b/2022/day16-part2/Cargo.toml new file mode 100644 index 0000000..342c67f --- /dev/null +++ b/2022/day16-part2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day16-part2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/2022/day16-part2/src/main.rs b/2022/day16-part2/src/main.rs new file mode 100644 index 0000000..b6ac3c6 --- /dev/null +++ b/2022/day16-part2/src/main.rs @@ -0,0 +1,293 @@ +use std::collections::{HashMap, VecDeque}; + +type Name = (char, char); + +#[derive(Debug)] +struct Valve { + name: (char, char), + flow_rate: i32, + tunnels: Vec, +} + +impl Valve { + fn print(&self) { + print!("{}{} -- {:3} -- ", self.name.0, self.name.1, self.flow_rate); + for t in &self.tunnels { + print!("{}{}, ", t.0, t.1); + } + println!(); + } +} + +fn main() { + // Use command line arguments to specify the input filename. + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + panic!("Usage: ./main \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 --- + + // Parse the input. + // Collect all nodes by name to a big map. + let mut nodes: HashMap = HashMap::new(); + for line in input.lines() { + // Split word-by-word. + let words = line.split_whitespace().collect::>(); + + // Determine this node's name. + let name = ( + words[1].chars().next().unwrap(), + words[1].chars().nth(1).unwrap(), + ); + + // Grab the list of outgoing nodes for this node and strip the whitespace. + // Returns "DD,II,BB". + let tunnel_nodes = line + .split("valve") + .nth(1) + .unwrap() + .chars() + .filter(|e| *e != ' ' && *e != 's') + .collect::(); + // Next, split by ','. + // Returns ["DD", "II", "BB"]. + let tunnel_nodes = tunnel_nodes.split(',').collect::>(); + // Turn the vector into a vector of names. + // Returns [('D', 'D'), ('I', 'I'), ('B', 'B')]. + let tunnel_nodes: Vec = tunnel_nodes + .iter() + .map(|e| (e.chars().next().unwrap(), e.chars().nth(1).unwrap())) + .collect(); + + // Construct this node. + let node = Valve { + name, + flow_rate: words[4] + .strip_prefix("rate=") + .unwrap() + .strip_suffix(';') + .unwrap() + .parse() + .unwrap(), + tunnels: tunnel_nodes, + }; + // Add this node to the big map. + nodes.insert(name, node); + } + + // In order to properly calculate the optimal path and valve order + // we need to first compute the cost getting from any node A to any + // other node B, i.e. perform pathfinding. + // We will precompute the results for faster lookup times later. + let mut distances: HashMap<(Name, Name), i32> = HashMap::new(); + for an in nodes.keys() { + // Technically we're performing Dijkstra's shortest path algorithm. + // Since the edges all have weight 1 this devolves to simple breadth-first search. + + // Keep track of all nodes in a queue. + let mut q: VecDeque<(Name, i32)> = VecDeque::new(); + // Add the current node to that queue. + q.push_back((*an, 0)); + + loop { + // Queue empty? Then we're done. + if q.is_empty() { + break; + } + + // Grab the next element (n) and its distance (d) from the queue. + let (n, d) = q.pop_front().unwrap(); + + // Since this is Dijkstra's algorithm, nodes are only visited once. + // Therefore, add this node to the proper result. + distances.insert((*an, n), d); + + // Next, look at all of this node's neighbors and add them to the queue. + for neighbor in &nodes[&n].tunnels { + // Only add them if they haven't already been visited. + if distances.get(&(*an, *neighbor)).is_none() { + q.push_back((*neighbor, d + 1)); + } + } + } + } + + // Keep track of all nodes with non-zero flow_rate. + let mut non_zero_nodes: Vec = nodes + .iter() + .filter(|(_, v)| v.flow_rate > 0) + .map(|(k, _)| *k) + .collect(); + + // Ensure each run is deterministic. + non_zero_nodes.sort(); + + // Now check through all possible permutations of non-zero nodes using a recursive function. + // We assume 'AA' has zero flow_rate (it should). + let mut optimum: i32 = 0; + generate_permutation( + &nodes, + &distances, + &mut non_zero_nodes, + &mut vec![('A', 'A')], + &mut vec![('A', 'A')], + 26, + 0, + 0, + 0, + &mut optimum, + ); + + println!("Optimal pressure release: {}", optimum); +} + +fn generate_permutation( + nodes: &HashMap, + distances: &HashMap<(Name, Name), i32>, + source: &mut Vec, + dest_a: &mut Vec, + dest_b: &mut Vec, + time_left: i32, + busy_a: i32, + busy_b: i32, + pressure: i32, + optimum: &mut i32, +) { + + // print!("Source: "); + // print_name_list(source); + // print!("A dest: "); + // print_name_list(dest_a); + // print!("B dest: "); + // print_name_list(dest_b); + // println!("Pressure: {} | Optimum: {}\n", pressure, optimum); + + // Are we done here? + // If there are only 2 units of time (or less) left we're done here. + // We're obviously also done if the source is empty. + if time_left <= 2 || source.is_empty() { + // Check if this is better and store the optimal result. + if pressure > *optimum { + print!("A dest: "); + print_name_list(dest_a); + print!("B dest: "); + print_name_list(dest_b); + println!("New Optimum: {}\n", pressure); + *optimum = pressure; + } + // Obviously, return early. + return; + } + // Differentiate between four cases: + // 1: Both elephant and us are busy. => Nothing to do, time passes. + // 2/3: Either elephant or us are free. => Choose next destination for free + + // If both actors are busy, simply let time pass. + if busy_a > 0 && busy_b > 0 { + let pass = std::cmp::min(busy_a, busy_b); + // Call recursively. + generate_permutation( + nodes, + distances, + source, + dest_a, + dest_b, + time_left - pass, + busy_a - pass, + busy_b - pass, + pressure, + optimum, + ); + } else if busy_a == 0 { + // First actor is free to choose their next destination. + + for i in 0..source.len() { + // Remove the current element from the vector. + let e = source.remove(i); + + // Determine the distance between the last two nodes. + let add_dist = distances[&(*dest_a.last().unwrap(), e)]; + + // Put the element onto the destination. + dest_a.push(e); + + // Determine how much time will have passed until this node is ready. + let new_time = time_left - (1 + add_dist); + // Calculate how much pressure we save. + let add_pressure = new_time * nodes[&e].flow_rate; + + // Call recursively, but don't let any time pass. + // If busy_b == 0, the next recursive step will choose for the other actor. + // If busy_b > 0, the next recursive step will simply count down the time. + + generate_permutation( + nodes, + distances, + source, + dest_a, + dest_b, + time_left, + 1 + add_dist, + busy_b, + pressure + add_pressure, + optimum, + ); + + // Remove the element from the destination. + dest_a.pop(); + // And reinsert the element back into the vector, at the same precise location. + source.insert(i, e); + } + + } else if busy_b == 0 { + // Second actor is free to choose their next destination. + + // Iterate through all elements remaining in the source. + for i in 0..source.len() { + // Remove the current element from the vector. + let e = source.remove(i); + + // Determine the distance between the last two nodes. + let add_dist = distances[&(*dest_b.last().unwrap(), e)]; + + // Put the element onto the destination. + dest_b.push(e); + + // Determine how much time will have passed until this node is ready. + let new_time = time_left - (1 + add_dist); + // Calculate how much pressure we save. + let add_pressure = new_time * nodes[&e].flow_rate; + + // Call recursively, but don't let any time pass. + generate_permutation( + nodes, + distances, + source, + dest_a, + dest_b, + time_left, + busy_a, + 1 + add_dist, + pressure + add_pressure, + optimum, + ); + + // Remove the element from the destination. + dest_b.pop(); + // And reinsert the element back into the vector, at the same precise location. + source.insert(i, e); + } + + } +} + +fn print_name_list(list: &Vec) { + for n in list { + print!("{}{}, ", n.0, n.1); + } + println!(); +}