aoc/2022/16/src/main.rs

407 lines
15 KiB
Rust

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::<Data>::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::<usize, u32>::new();
let mut node_indexes = std::vec::Vec::<usize>::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::<usize>::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::<usize>::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<Data>, start: usize, time: u32, nodes: &std::vec::Vec<usize>, 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<Data>, start: usize, time: u32, visited: &std::vec::Vec<usize>, path_cache: &mut std::collections::HashMap<(usize, usize), usize>) -> std::vec::Vec<std::vec::Vec<usize>> {
let mut results = std::vec::Vec::<std::vec::Vec<usize>>::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<usize>, b: &std::vec::Vec<usize>, tree: &Tree<Data>, 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<Data>, 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::<usize>::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<usize> = 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<Data>, start: usize, time_remaining: u32, look_forward: usize, exclude: &std::vec::Vec<usize>) -> 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<Data>, line: &str) {
if line.eq("") {
return;
}
let words: std::vec::Vec::<&str> = line.split(' ').collect();
let id: std::vec::Vec<char> = 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<char> = 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 => {},
};
}
}