advent-of-code/2022/day21-part1/src/main.rs

108 lines
4.0 KiB
Rust

use itertools::Itertools;
use std::collections::HashMap;
use id_arena::{Arena, Id};
type MonkeyNumber = i64;
type NodeId = Id<Node>;
#[derive(Debug)]
enum Node {
Uninitialized,
Unit(MonkeyNumber),
Add(NodeId, NodeId),
Sub(NodeId, NodeId),
Mul(NodeId, NodeId),
Div(NodeId, NodeId),
}
impl Node {
/// Evaluates the expression tree with the given Node as root node and returns the calculated result.
///
/// Uses recursion under-the-hood to implement the evaluation.
/// The recursive strategy is essentially a depth-first-search algorithm.
fn evaluate(&self, nodes: &Arena<Node>) -> MonkeyNumber {
match self {
Node::Unit(value) => *value,
Node::Add(lhs, rhs) => nodes[*lhs].evaluate(nodes) + nodes[*rhs].evaluate(nodes),
Node::Sub(lhs, rhs) => nodes[*lhs].evaluate(nodes) - nodes[*rhs].evaluate(nodes),
Node::Mul(lhs, rhs) => nodes[*lhs].evaluate(nodes) * nodes[*rhs].evaluate(nodes),
Node::Div(lhs, rhs) => nodes[*lhs].evaluate(nodes) / nodes[*rhs].evaluate(nodes),
Node::Uninitialized => panic!("encountered uninitialized Node during evaluation"),
}
}
}
fn main() {
// Use command line arguments to specify the input filename.
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
panic!("Usage: ./main <input-file>\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::<Node>::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 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::<MonkeyNumber>().ok();
// 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"),
};
}
// Evaluate the expression tree and print the result.
println!("Result: {}", nodes[identifiers[b"root"]].evaluate(&nodes));
}