This document provides an educational walkthrough of an equation solver program designed to tackle a specific puzzle challenge. The program solves mathematical equations where a target value must be constructed using a set of coefficient values combined with different operations.
The problem presents equations in the format result: coeff1 coeff2 coeff3...
where we need to find valid ways to combine coefficients to achieve the result value. The solution approach uses recursive backtracking to explore different operation combinations.
The operations available are:
a * b
a + b
a = 5, b = 7
→ 57
(introduced in part 2)Our strategy will be to recursively try each operation with each coefficient, exploring all possible paths until we find a valid solution that matches the target result.
The first building block is a structure to represent an equation:
#[derive(Debug)]
pub(crate) struct Equation {
result: u64,
coeff: Rc<[u64]>
}
This structure stores:
result
: The target value we need to achievecoeff
: The coefficients available for operations, stored as a reference-counted array for efficient memory managementUsing Rc<[u64]>
is a design choice allowing for shared ownership of the coefficients without unnecessary copying during recursive calls.
We need to parse input strings like "190: 10 19"
into Equation
structures. The program uses the nom
parsing library for robust, declarative parsing:
fn parse_equation(s: &str) -> IResult<&str, Equation> {
map(
separated_pair(
u64,
tuple(( space0, tag(":") )),
tuple(( space0, separated_list1(space1,u64) ))
),
|(result, (_, coeff))| Equation { result, coeff: coeff.into() }
)(s)
}
This parser:
Equation
structImplementing the FromStr
trait enables convenient parsing with Rust’s .parse()
method:
impl FromStr for Equation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match parse_equation(s) {
Ok(e) => Ok(e.1),
Err(e) => Err(e.to_string()),
}
}
}
The heart of the solution is the recursive solver function which explores all possible ways to combine coefficients:
fn solve(total: u64, coeff: &[u64], cop: bool) -> Option<u64> {
fn ct(a:u64, b:u64) -> u64 { format!("{}{}",a,b).parse::<u64>().unwrap() }
let idx = coeff.len() - 1;
if idx == 0 { return Some(coeff[idx]) }
let res_1 = Self::solve(total / coeff[idx], &coeff[..idx], cop).map(|s| s * coeff[idx]);
let res_2 = if total >= coeff[0] {
Self::solve(total - coeff[idx], &coeff[..idx], cop).map(|s| s + coeff[idx])
} else { None };
let res_3 = if cop && total >= coeff[0] {
Self::solve((total - coeff[idx])/10u64.pow(coeff[idx].ilog10()+1), &coeff[..idx], cop)
.map(|s| ct(s, coeff[idx]))
} else { None };
match (res_1 == Some(total), res_2 == Some(total), res_3 == Some(total)) {
(true, _, _) => res_1,
(_, true, _) => res_2,
(_, _, true) => res_3,
_ => None,
}
}
The algorithm works by:
cop
is true): Handle digit concatenation as a special operationThe approach effectively creates a decision tree where each level tries all operations with the next coefficient.
The solution includes several optimizations:
let res_2 = if total >= coeff[0] {
Self::solve(total - coeff[idx], &coeff[..idx], cop).map(|s| s + coeff[idx])
} else { None };
match (res_1 == Some(total), res_2 == Some(total), res_3 == Some(total)) {
(true, _, _) => res_1,
(_, true, _) => res_2,
(_, _, true) => res_3,
_ => None,
}
Self::solve(total / coeff[idx], &coeff[..idx], cop)
The Equation
struct provides a clean public interface through the solver
method:
pub(crate) fn solver(&self, cop: bool) -> Option<u64> {
Self::solve(self.result, &self.coeff, cop)
}
This design:
cop
parameterThe main program ties everything together:
fn main() {
let input = std::fs::read_to_string("src/bin/day7/input.txt").expect("msg");
let equations = input.lines()
.map(|line| line.parse::<Equation>().unwrap())
.collect::<Vec<_>>();
let t = Instant::now();
let sum = equations.iter()
.filter_map(|eq| eq.solver(false))
.sum::<u64>();
println!("Part 1: total calibration result is {sum} - {:?}", t.elapsed());
let t = Instant::now();
let sum = equations.iter()
.filter_map(|eq| eq.solver(true))
.sum::<u64>();
println!("Part 2: total calibration result with CompOp is {sum} - {:?}", t.elapsed());
}
The program:
Equation
The implementation includes unit tests for the parser to validate it handles various input formats:
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_input() {
assert!("190: 10 19".parse::<Equation>().is_ok());
assert!("3267: 81 40 27".parse::<Equation>().is_ok());
assert!("83:17 5".parse::<Equation>().is_ok());
assert!("83 :17 5".parse::<Equation>().is_ok());
assert!("83 : 17 5".parse::<Equation>().is_ok());
assert!("83 : ".parse::<Equation>().is_err());
assert!("363816188802: 5 601 3 603 2 2 93 6 3 5".parse::<Equation>().is_ok());
}
}
This testing ensures the parser is robust against different spacing patterns and input variations.
This equation solver demonstrates several advanced programming concepts:
The program effectively solves a challenging problem by breaking it down into manageable components, each with clear responsibilities and interfaces. The recursive approach elegantly handles the combinatorial complexity of trying different operations to reach the target result.