diff --git a/2022/16/Cargo.lock b/2022/16/Cargo.lock new file mode 100644 index 0000000..716ffed --- /dev/null +++ b/2022/16/Cargo.lock @@ -0,0 +1,30 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "common" +version = "0.1.0" + +[[package]] +name = "day16" +version = "0.1.0" +dependencies = [ + "common", + "itertools", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] diff --git a/2022/16/Cargo.toml b/2022/16/Cargo.toml new file mode 100644 index 0000000..9df4c30 --- /dev/null +++ b/2022/16/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "day16" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +common = { path = "../common" } +itertools = "0.10" \ No newline at end of file diff --git a/2022/16/input b/2022/16/input new file mode 100644 index 0000000..b8c0b14 --- /dev/null +++ b/2022/16/input @@ -0,0 +1,62 @@ +Valve OM has flow rate=0; tunnels lead to valves AA, EZ +Valve ZZ has flow rate=0; tunnels lead to valves LR, QY +Valve NC has flow rate=0; tunnels lead to valves KX, QI +Valve QI has flow rate=5; tunnels lead to valves TX, NC, QS, HY, UX +Valve QS has flow rate=0; tunnels lead to valves CY, QI +Valve FP has flow rate=0; tunnels lead to valves IW, SJ +Valve ZR has flow rate=0; tunnels lead to valves ID, KC +Valve YR has flow rate=21; tunnels lead to valves RS, OT, FV +Valve SJ has flow rate=23; tunnel leads to valve FP +Valve QY has flow rate=0; tunnels lead to valves ZZ, NU +Valve KD has flow rate=13; tunnels lead to valves WY, ZP +Valve GT has flow rate=0; tunnels lead to valves SG, PD +Valve DB has flow rate=0; tunnels lead to valves TX, MX +Valve KW has flow rate=0; tunnels lead to valves AK, HM +Valve TX has flow rate=0; tunnels lead to valves QI, DB +Valve YX has flow rate=0; tunnels lead to valves HY, AA +Valve NA has flow rate=0; tunnels lead to valves NU, KS +Valve ST has flow rate=0; tunnels lead to valves YO, PD +Valve UX has flow rate=0; tunnels lead to valves QI, OT +Valve OT has flow rate=0; tunnels lead to valves UX, YR +Valve AK has flow rate=0; tunnels lead to valves KW, PD +Valve UC has flow rate=0; tunnels lead to valves YH, KC +Valve FF has flow rate=0; tunnels lead to valves YO, IN +Valve GN has flow rate=0; tunnels lead to valves CY, MX +Valve KK has flow rate=0; tunnels lead to valves WY, YO +Valve PD has flow rate=10; tunnels lead to valves GT, ID, HW, ST, AK +Valve LR has flow rate=18; tunnels lead to valves ZZ, NM, SG, YK +Valve CY has flow rate=14; tunnels lead to valves VB, GN, QS, FV +Valve YH has flow rate=0; tunnels lead to valves UC, VQ +Valve RS has flow rate=0; tunnels lead to valves MX, YR +Valve YO has flow rate=20; tunnels lead to valves FF, NM, KK, ST, ZU +Valve HQ has flow rate=0; tunnels lead to valves AA, MX +Valve UE has flow rate=0; tunnels lead to valves HM, IN +Valve NM has flow rate=0; tunnels lead to valves LR, YO +Valve KX has flow rate=7; tunnels lead to valves NC, UZ, XK, PV +Valve IW has flow rate=0; tunnels lead to valves VQ, FP +Valve IN has flow rate=22; tunnels lead to valves FF, UE +Valve WY has flow rate=0; tunnels lead to valves KK, KD +Valve HY has flow rate=0; tunnels lead to valves YX, QI +Valve AA has flow rate=0; tunnels lead to valves KS, OM, XO, HQ, YX +Valve ZU has flow rate=0; tunnels lead to valves YO, NU +Valve YK has flow rate=0; tunnels lead to valves ZP, LR +Valve XK has flow rate=0; tunnels lead to valves XO, KX +Valve VB has flow rate=0; tunnels lead to valves CY, UZ +Valve ZP has flow rate=0; tunnels lead to valves KD, YK +Valve VQ has flow rate=11; tunnels lead to valves YH, IW, EZ +Valve HW has flow rate=0; tunnels lead to valves NU, PD +Valve NU has flow rate=8; tunnels lead to valves ZU, UD, NA, HW, QY +Valve UZ has flow rate=0; tunnels lead to valves KX, VB +Valve PV has flow rate=0; tunnels lead to valves DY, KX +Valve MX has flow rate=6; tunnels lead to valves HQ, DB, DY, RS, GN +Valve KS has flow rate=0; tunnels lead to valves NA, AA +Valve UD has flow rate=0; tunnels lead to valves NU, IO +Valve FV has flow rate=0; tunnels lead to valves YR, CY +Valve SG has flow rate=0; tunnels lead to valves LR, GT +Valve HM has flow rate=24; tunnels lead to valves KW, UE +Valve XO has flow rate=0; tunnels lead to valves AA, XK +Valve KC has flow rate=12; tunnels lead to valves IO, UC, ZR +Valve IO has flow rate=0; tunnels lead to valves UD, KC +Valve DY has flow rate=0; tunnels lead to valves PV, MX +Valve ID has flow rate=0; tunnels lead to valves PD, ZR +Valve EZ has flow rate=0; tunnels lead to valves VQ, OM diff --git a/2022/16/src/main.rs b/2022/16/src/main.rs new file mode 100644 index 0000000..8418ae4 --- /dev/null +++ b/2022/16/src/main.rs @@ -0,0 +1,406 @@ +use itertools::Itertools; +use std::cmp::Ordering; +use std::str::FromStr; + +use common::tree::Tree; + +fn main() { + let input_file = common::parse_args_input_file(&mut std::env::args()); + let contents = std::fs::read_to_string(input_file).expect("Failed to read input file"); + + // Abusing tree by not setting parents and just using children for navigation + let mut tree = Tree::::new(); + for line in contents.lines() { + parse_line(&mut tree, line); + } + + for (index, node) in tree.nodes.iter().enumerate() { + print!("Node '{}' at index {} leads to {} nodes", node.data.name(), index, node.children.len()); + if node.children.len() > 0 { + print!(":"); + for (idx, child) in node.children.iter().enumerate() { + print!(" '{}' at index {}", tree.get(*child).unwrap().data.name(), child); + if idx < node.children.len() - 1 { + print!(","); + } + } + } + print!("\n"); + } + + let mut current_node = 0; + let mut nodes_with_working_valves = std::collections::HashMap::::new(); + let mut node_indexes = std::vec::Vec::::new(); + for (index, node) in tree.nodes.iter().enumerate() { + if node.data.name().eq("AA") { + current_node = index; + } + if node.data.flow_rate > 0 { + nodes_with_working_valves.insert(index, node.data.flow_rate); + node_indexes.push(index); + } + } + + // (from,to) -> distance + // from is always the lowest of the pair, single distance to->from is the same as from->to + let mut path_cache = std::collections::HashMap::<(usize, usize), usize>::new(); + for x in 0..nodes_with_working_valves.len() { + let mut left = std::cmp::min(current_node, node_indexes[x]); + let mut right = std::cmp::max(current_node, node_indexes[x]); + match tree.find_path(left, right) { + Some(v) => { + path_cache.insert((left, right), v.len()); + }, + None => { unreachable!() }, + }; + for y in 0..nodes_with_working_valves.len() { + if x == y { + continue; + } + left = std::cmp::min(node_indexes[x], node_indexes[y]); + right = std::cmp::min(node_indexes[x], node_indexes[y]); + if path_cache.contains_key(&(left, right)) { + continue; + } + match tree.find_path(left, right) { + Some(v) => { + path_cache.insert((left, right), v.len()); + }, + None => { unreachable!() }, + }; + } + } + println!("{} elements in path_cache", path_cache.len()); + + // The greedy estimate isn't not always the optimal choice, since it doesn't + // consider what other moves may be made afterwards. + // I don't know how to get the absolute best. + // + // The brute force option is to generate n! combinations of nodes with working + // valves, and simulate each one. + let look_forward = 15; + let mut current_pressure_outgoing = 0; + let mut pressure_released = 0; + let mut time_remaining = 30; + let mut time = std::time::Instant::now(); + // let exclude = std::vec::Vec::::new(); + // let mut next = estimate_path_brute(&tree, current_node, time_remaining, look_forward, &mut path_cache); + // while next.is_some() { + // let (next_id, _) = next.unwrap(); + // let left = std::cmp::min(current_node, next_id); + // let right = std::cmp::max(current_node, next_id); + // assert!(path_cache.contains_key(&(left, right))); + // let distance = *path_cache.get(&(left, right)).unwrap() as u32; + // time_remaining -= distance; + // println!("From {} to {} in {} time at outgoing pressure {}", + // tree.nodes[current_node].data.name(), + // tree.nodes[next_id].data.name(), + // distance, current_pressure_outgoing, + // ); + // pressure_released += current_pressure_outgoing * distance; + // current_pressure_outgoing += tree.nodes[next_id].data.flow_rate; + // current_node = next_id; + // tree.nodes[current_node].data.open = true; + // next = estimate_path_brute(&tree, current_node, time_remaining, look_forward, &mut path_cache); + // } + // pressure_released += time_remaining * current_pressure_outgoing; + // println!("[PART 1, Greedy] Pressure released {} calculated in {}us", pressure_released, time.elapsed().as_micros()); + + // let path = vec![3, 1, 9, 7, 4, 2]; + // println!("{:?} scores {}", path, score_path(&tree, 0, 30, &path, &mut path_cache)); + // for node in tree.nodes.iter_mut() { + // node.data.open = false; + // } + let mut best_path_score = 0; + let mut best_path = None; + time = std::time::Instant::now(); + let paths = build_paths(&tree, 0, 30, &std::vec::Vec::::new(), &mut path_cache); + for i in &paths { + let score = score_path(&tree, 0, 30, &i, &mut path_cache); + if score > best_path_score { + println!("{:?} is new high score {}", i, score); + best_path_score = score; + best_path = Some(i.clone()); + } + } + println!("[PART 1] {} calculated in {}us from {} options", best_path_score, time.elapsed().as_micros(), paths.len()); +} + +fn score_path(tree: &Tree, start: usize, time: u32, nodes: &std::vec::Vec, path_cache: &mut std::collections::HashMap<(usize, usize), usize>) -> u32 { + let mut current_score_adjustment = 0; + let mut score = 0; + let mut remaining = time; + let mut current_node = start; + for index in nodes.iter() { + let left = std::cmp::min(current_node, *index); + let right = std::cmp::max(current_node, *index); + if !path_cache.contains_key(&(left, right)) { + match tree.find_path(left, right) { + Some(path) => { + path_cache.insert((left, right), path.len()); + }, + None => {unreachable!()}, + }; + } + let distance = *path_cache.get(&(left, right)).unwrap() as u32; + remaining -= distance; + // println!("From {} to {} in {} time at outgoing pressure {}", + // tree.nodes[current_node].data.name(), + // tree.nodes[*index].data.name(), + // distance, current_score_adjustment, + // ); + score += current_score_adjustment * distance; + current_score_adjustment += tree.nodes[*index].data.flow_rate; + current_node = *index; + } + score += remaining * current_score_adjustment; + return score; +} + +fn build_paths(tree: &Tree, start: usize, time: u32, visited: &std::vec::Vec, path_cache: &mut std::collections::HashMap<(usize, usize), usize>) -> std::vec::Vec> { + let mut results = std::vec::Vec::>::new(); + let mut has_new = false; + for (index, node) in tree.nodes.iter().enumerate() { + if tree.nodes[index].data.open { + // Already open, continue searching. + continue; + } + if tree.nodes[index].data.flow_rate <= 0 { + // Busted. + continue; + } + if visited.contains(&index) { + continue; + } + let left = std::cmp::min(index, start); + let right = std::cmp::max(index, start); + if !path_cache.contains_key(&(left, right)) { + match tree.find_path(left, right) { + Some(path) => { path_cache.insert((left, right), path.len()); }, + None => { unreachable!(); }, + }; + } + let d = *path_cache.get(&(left, right)).unwrap() as u32; + // If we can't get there, it's not really an option + if d < time { + // println!("Considering path via {} ({})", node.data.name(), index); + let mut new_visited = visited.clone(); + new_visited.push(index); + let mut new_results = build_paths(tree, index, time - d, &new_visited, path_cache); + if new_results.len() > 0 { + has_new = true; + // Arbitrarily keep half the good options + new_results.sort_by(|a, b| compare_path(b, a, tree, index, time - d, path_cache)); + let end = std::cmp::min( + new_results.len(), + std::cmp::max(new_results.len()+1/2, 15) + ); + for i in 0..end { + results.push(new_results[i].clone()); + } + } + } + } + if !has_new { + results.push(visited.clone()); + } + return results; +} + +fn compare_path(a: &std::vec::Vec, b: &std::vec::Vec, tree: &Tree, start: usize, time_remaining: u32, path_cache: &mut std::collections::HashMap<(usize, usize), usize>) -> Ordering { + let score_a = score_path(tree, start, time_remaining, a, path_cache); + let score_b = score_path(tree, start, time_remaining, b, path_cache); + if score_a > score_b { + return Ordering::Greater; + } + else if score_a < score_b { + return Ordering::Less; + } + return Ordering::Equal; +} + +fn estimate_path_brute(tree: &Tree, start: usize, time_remaining: u32, look_forward: usize, path_cache: &mut std::collections::HashMap<(usize,usize),usize>) -> Option<(usize, u32)> { + let mut options = std::vec::Vec::::new(); + for (index, node) in tree.nodes.iter().enumerate() { + if tree.nodes[index].data.open { + // Already open, continue searching. + continue; + } + if tree.nodes[index].data.flow_rate <= 0 { + // Busted. + continue; + } + options.push(index); + } + let mut estimate = 0; + let mut next: Option = None; + let x = std::cmp::min(look_forward + 1, options.len()); + for permutation in options.into_iter().permutations(x) { + let mut last = start; + let mut tr = time_remaining; + let mut est = 0; + let mut valid = true; + // println!("{:?}", permutation); + for element in &permutation { + let left = std::cmp::min(last, *element); + let right = std::cmp::max(last, *element); + if !path_cache.contains_key(&(left, right)) { + let p = tree.find_path(left, right); + path_cache.insert((left, right), p.unwrap().len()); + } + match path_cache.get(&(left, right)) { + Some(length) => { + if (*length as u32) >= tr { + valid = false; + break; + } + est += (tr - *length as u32) * tree.nodes[*element].data.flow_rate; + last = *element; + tr -= *length as u32; + }, + None => { + println!("No path from {} to {}", left, right); + valid = false; break; + } + }; + } + if valid { + // println!("Path {:?} estimate {}", permutation, est); + if est > estimate { + next = Some(permutation[0]); + estimate = est; + } + } + } + if next.is_some() { + return Some((next.unwrap(), estimate)); + } + return None; +} + +fn estimate_path_greedy(tree: &Tree, start: usize, time_remaining: u32, look_forward: usize, exclude: &std::vec::Vec) -> Option<(usize, u32)> { + let mut next_index: Option<(usize, u32)> = None; + let mut estimated_release = 0; + let mut current_node = start; + for (index, node) in tree.nodes.iter().enumerate() { + if exclude.contains(&index) { + continue; + } + if tree.nodes[index].data.open { + // Already open, continue searching. + continue; + } + if tree.nodes[index].data.flow_rate <= 0 { + // Busted. + continue; + } + + let path = match tree.find_path(current_node, index) { + Some(v) => { v }, + None => { + //println!("No path from {} to {}", current_node, index); + continue; + }, + }; + + // It takes 1 minute to open the valve, but since our path includes the starting + // node that we are already on, the estimate to open would be path.len() - 1 + 1 + let time_to_open = path.len() as u32; + if time_to_open >= time_remaining { + // println!("It would take too long ({}) to get to and open {}", time_to_open, tree.nodes[index].data.name()); + continue; + } + + let mut estimate = (time_remaining - time_to_open - 1) as u32 * (node.data.flow_rate); + // Penalize dead ends a bit... + // if node.children.len() < 2 { + // estimate -= std::cmp::min(10, estimate - 1); + // } + + // Penalize distance + // estimate -= std::cmp::min(estimate - 1, 50 * path.len() as u32); + + // Would it be useful to look forward by one or two nodes? + let mut lf = look_forward; + let mut next_exclude = exclude.clone(); + next_exclude.push(index); + while lf > 0 { + lf -= 1; + match estimate_path_greedy(tree, index, time_remaining - time_to_open, lf, &next_exclude) { + Some((v, e)) => { + estimate += e; + next_exclude.push(v); + }, + None => {}, + }; + } + // println!("{:?}: {}", next_exclude, estimate); + + // println!("{} estimates {}: {:?}", node.data.name(), estimate, path); + if estimate > estimated_release { + estimated_release = estimate; + next_index = Some((index, estimate)); + } + } + if next_index.is_some() { + println!("LF{}, Estimate {} ({}) releases {}", look_forward, tree.nodes[next_index.unwrap().0].data.name(), next_index.unwrap().0, next_index.unwrap().1); + } + return next_index; +} + +struct Data { + open: bool, + id: [char; 2], + flow_rate: u32, +} + +impl Data { + fn name(&self) -> String { + let mut s = String::new(); + s.push(self.id[0]); + s.push(self.id[1]); + return s; + } +} + +fn parse_line(tree: &mut Tree, line: &str) { + if line.eq("") { + return; + } + let words: std::vec::Vec::<&str> = line.split(' ').collect(); + let id: std::vec::Vec = words[1].chars().collect(); + let rate_unparsed = words[4]; + let terminator = ", ".to_string(); + let towards_unparsed = &words[9..]; + let mut data = Data { + open: false, + id: [id[0], id[1]], + flow_rate: 0, + }; + let flow_rate_end = rate_unparsed.find(';').unwrap(); + data.flow_rate = u32::from_str(&rate_unparsed[5..flow_rate_end]).expect("Failed to parse flow rate"); + let new_id = tree.nodes.len(); + tree.insert(data, None); + for word in towards_unparsed { + let chars: std::vec::Vec = word.chars().collect(); + let other_id = [chars[0], chars[1]]; + let mut other_index = None; + for (index, node) in tree.nodes.iter().enumerate() { + if node.data.id.eq(&other_id) { + other_index = Some(index); + break; + } + } + match other_index { + Some(v) => { + if !tree.nodes[new_id].children.contains(&v) { + tree.nodes[new_id].children.push(v); + } + if !tree.nodes[v].children.contains(&new_id) { + tree.nodes[v].children.push(new_id); + } + }, + None => {}, + }; + } +} diff --git a/2022/16/test_input b/2022/16/test_input new file mode 100644 index 0000000..9f30acc --- /dev/null +++ b/2022/16/test_input @@ -0,0 +1,10 @@ +Valve AA has flow rate=0; tunnels lead to valves DD, II, BB +Valve BB has flow rate=13; tunnels lead to valves CC, AA +Valve CC has flow rate=2; tunnels lead to valves DD, BB +Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE +Valve EE has flow rate=3; tunnels lead to valves FF, DD +Valve FF has flow rate=0; tunnels lead to valves EE, GG +Valve GG has flow rate=0; tunnels lead to valves FF, HH +Valve HH has flow rate=22; tunnel leads to valve GG +Valve II has flow rate=0; tunnels lead to valves AA, JJ +Valve JJ has flow rate=21; tunnel leads to valve II diff --git a/2022/common/src/lib.rs b/2022/common/src/lib.rs index 33172a5..56eac39 100644 --- a/2022/common/src/lib.rs +++ b/2022/common/src/lib.rs @@ -1,3 +1,5 @@ +pub mod tree; + pub fn parse_args_input_file(args: &mut std::env::Args) -> String { let mut input_file = Some("input".to_string()); while args.len() > 0 { diff --git a/2022/common/src/tree.rs b/2022/common/src/tree.rs new file mode 100644 index 0000000..1633c10 --- /dev/null +++ b/2022/common/src/tree.rs @@ -0,0 +1,133 @@ +pub struct Node { + pub parent: Option, + pub data: T, + pub children: std::vec::Vec, +} + +impl Node { + +} + +pub struct Tree { + pub nodes: std::vec::Vec>, +} + +impl Tree { + pub fn new() -> Tree { + return Self { + nodes: std::vec::Vec::>::new(), + }; + } + + + pub fn get(&self, index: usize) -> Option<&Node> { + return self.nodes.get(index); + } + + pub fn insert(&mut self, data: T, parent: Option) { + let node = Node:: { + data: data, + parent: parent, + children: std::vec::Vec::::new(), + }; + let new_id = self.nodes.len(); + self.nodes.push(node); + if parent.is_some() { + match self.nodes.get_mut(parent.unwrap()) { + Some(v) => { + v.children.push(new_id); + }, + None => { + assert!(false, "A parent was given, but not found in the list of nodes"); + } + }; + } + } + + // Implementation of A* + // @see https://en.wikipedia.org/wiki/A*_search_algorithm + pub fn find_path(&self, start: usize, end: usize) -> Option> { + let mut open_set = std::collections::VecDeque::::new(); + open_set.push_back(start); + + let mut came_from = std::collections::HashMap::::new(); + + // Cheapest path from start to item n currently known + let mut scores = std::collections::HashMap::::new(); + scores.insert(start, 0); + + // guesses of cost of path from start to end via n + let mut estimates = std::collections::HashMap::::new(); + + while open_set.len() > 0 { + // Get the node in the openSet with the lowest fScore to test + let current = path_get_next_node_to_test(&open_set, &estimates); + if current == end { + return Some(path_from_previous_nodes(&came_from, current)); + } + open_set.retain(|&x| x != current); + for neighbour in &self.nodes[current].children { + assert!(scores.contains_key(¤t)); + let score = scores.get(¤t).unwrap() + 1; // 1 is normally the weight of the edge from current to neighbour + let neighbour_score = match scores.get(&neighbour) { + None => { u32::MAX }, + Some(v) => { *v }, + }; + if score < neighbour_score { + came_from.insert(*neighbour, current); + scores.insert(*neighbour, score); + // @TODO + // guess cost to reach end via this node: h(n), where h(n) is + // a heuristic function; however, I don't have an idea of what + // those heuristics may be at this time + estimates.insert(*neighbour, score + 10); + if !open_set.contains(&neighbour) { + open_set.push_back(*neighbour); + } + } + } + } + return None; + } +} + +fn path_from_previous_nodes(from: &std::collections::HashMap, end: usize) -> std::vec::Vec { + let mut path = std::vec::Vec::::new(); + path.push(end); + let mut current = from.get(&end); + while current.is_some() { + path.insert(0, *current.unwrap()); + current = from.get(&*current.unwrap()); + } + return path; +} + +fn path_get_next_node_to_test(open: &std::collections::VecDeque, estimates: &std::collections::HashMap) -> usize { + let mut current: Option = None; + let mut current_value = u32::MAX; + for n in open.iter() { + let score = match estimates.get(n) { + None => { + u32::MAX + }, + Some(v) => { + *v + }, + }; + if current.is_none() || score < current_value { + current = Some(*n); + current_value = score; + } + } + return current.unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + _ = Tree::::new(); + } +}