diff --git a/2022/day21-part2/Cargo.toml b/2022/day21-part2/Cargo.toml new file mode 100644 index 0000000..4611185 --- /dev/null +++ b/2022/day21-part2/Cargo.toml @@ -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"] } diff --git a/2022/day21-part2/src/main.rs b/2022/day21-part2/src/main.rs new file mode 100644 index 0000000..212f84c --- /dev/null +++ b/2022/day21-part2/src/main.rs @@ -0,0 +1,236 @@ +use itertools::Itertools; +use std::collections::HashMap; + +use id_arena::{Arena, Id}; + +type MonkeyNumber = i64; +type NodeId = Id; + +#[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: 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: 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 = 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 --- + + // The complete collection of operation nodes. + let mut nodes = Arena::::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::().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); +}