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 => {}, }; } }