Rust 练习册 :字母算术与约束满足问题
在数学和逻辑学中,有一种有趣的谜题叫做字母算术(Alphametics),也被称为文字算术或密码算术。这类谜题将数字替换为字母,要求找出每个字母代表的数字,使得等式成立。最著名的例子是 “SEND + MORE = MONEY”。在 Exercism 的 “alphametics” 练习中,我们将实现一个通用的字母算术求解器,这不仅能帮助我们理解约束满足问题,还能深入学习 Rust 中的算法实现和组合优化。
问题背景
字母算术谜题有以下规则:
- 每个字母代表一个唯一的数字(0-9)
- 不同的字母必须代表不同的数字
- 相同的字母必须代表相同的数字
- 单词的首字母不能为 0
- 等式必须成立
例如,经典的 “SEND + MORE = MONEY” 谜题的解是:
S=9, E=5, N=6, D=7, M=1, O=0, R=8, Y=2
因为 9567 + 1085 = 10652。
让我们先看看练习提供的函数签名:
use std::collections::HashMap;
pub fn solve(input: &str) -> Option<HashMap<char, u8>> {
unimplemented!("Solve the alphametic {:?}", input)
}
我们需要实现一个函数,它能够:
- 解析输入的字母算术表达式
- 找到满足条件的数字分配方案
- 返回字母到数字的映射关系,如果无解则返回 None
算法思路
解决字母算术问题主要有两种方法:
- 暴力搜索:尝试所有可能的数字组合
- 约束传播:利用约束条件减少搜索空间
考虑到问题的复杂性,我们将采用暴力搜索结合剪枝的方法。
实现步骤
1. 解析输入
首先我们需要解析输入字符串,提取所有字母和数字关系:
use std::collections::HashMap;
pub fn solve(input: &str) -> Option<HashMap<char, u8>> {
// 移除空格并分割等式
let parts: Vec<&str> = input.split("==").collect();
if parts.len() != 2 {
return None;
}
let right = parts[1].trim();
let left_parts: Vec<&str> = parts[0].split('+').map(|s| s.trim()).collect();
// 收集所有唯一字母
let mut unique_chars = std::collections::HashSet::new();
let mut first_chars = std::collections::HashSet::new();
for part in &left_parts {
if part.is_empty() {
return None;
}
first_chars.insert(part.chars().next().unwrap());
for c in part.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
}
first_chars.insert(right.chars().next().unwrap());
for c in right.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
let chars: Vec<char> = unique_chars.into_iter().collect();
// 如果字母数超过10个,则无解
if chars.len() > 10 {
return None;
}
// ... 继续实现求解逻辑
None
}
2. 生成排列并验证
我们需要生成 0-9 数字的所有排列,并验证哪个满足等式:
use std::collections::HashMap;
pub fn solve(input: &str) -> Option<HashMap<char, u8>> {
// 解析输入
let parts: Vec<&str> = input.split("==").collect();
if parts.len() != 2 {
return None;
}
let right = parts[1].trim();
let left_parts: Vec<&str> = parts[0].split('+').map(|s| s.trim()).collect();
// 收集所有唯一字母
let mut unique_chars = std::collections::HashSet::new();
let mut first_chars = std::collections::HashSet::new();
for part in &left_parts {
if part.is_empty() {
return None;
}
first_chars.insert(part.chars().next().unwrap());
for c in part.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
}
first_chars.insert(right.chars().next().unwrap());
for c in right.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
let chars: Vec<char> = unique_chars.into_iter().collect();
// 如果字母数超过10个,则无解
if chars.len() > 10 {
return None;
}
// 生成 0-9 的所有排列
let digits: Vec<u8> = (0..10).collect();
let permutations = permute(&digits, chars.len());
// 尝试每个排列
for perm in permutations {
let mapping: HashMap<char, u8> = chars.iter().zip(perm.iter()).map(|(&c, &d)| (c, d)).collect();
// 检查首字母是否为0
if first_chars.iter().any(|&c| mapping[c] == 0) {
continue;
}
// 验证等式
if verify_equation(&left_parts, right, &mapping) {
return Some(mapping);
}
}
None
}
// 生成排列的辅助函数
fn permute(digits: &[u8], size: usize) -> Vec<Vec<u8>> {
let mut result = Vec::new();
let mut used = vec![false; digits.len()];
let mut current = Vec::with_capacity(size);
permute_helper(digits, size, &mut used, &mut current, &mut result);
result
}
fn permute_helper(
digits: &[u8],
size: usize,
used: &mut [bool],
current: &mut Vec<u8>,
result: &mut Vec<Vec<u8>>,
) {
if current.len() == size {
result.push(current.clone());
return;
}
for i in 0..digits.len() {
if !used[i] {
used[i] = true;
current.push(digits[i]);
permute_helper(digits, size, used, current, result);
current.pop();
used[i] = false;
}
}
}
// 验证等式的辅助函数
fn verify_equation(left_parts: &[&str], right: &str, mapping: &HashMap<char, u8>) -> bool {
let left_sum: u64 = left_parts
.iter()
.map(|part| word_to_number(part, mapping))
.sum();
let right_value = word_to_number(right, mapping);
left_sum == right_value
}
// 将单词转换为数字
fn word_to_number(word: &str, mapping: &HashMap<char, u8>) -> u64 {
let mut result = 0;
for c in word.chars() {
if let Some(&digit) = mapping.get(&c) {
result = result * 10 + digit as u64;
}
}
result
}
完整实现
考虑到性能优化,我们可以使用 itertools crate 来生成排列:
use std::collections::HashMap;
pub fn solve(input: &str) -> Option<HashMap<char, u8>> {
// 解析输入
let parts: Vec<&str> = input.split("==").collect();
if parts.len() != 2 {
return None;
}
let right = parts[1].trim();
let left_parts: Vec<&str> = parts[0].split('+').map(|s| s.trim()).collect();
// 收集所有唯一字母
let mut unique_chars = std::collections::HashSet::new();
let mut first_chars = std::collections::HashSet::new();
for part in &left_parts {
if part.is_empty() {
return None;
}
first_chars.insert(part.chars().next().unwrap());
for c in part.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
}
first_chars.insert(right.chars().next().unwrap());
for c in right.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
let chars: Vec<char> = unique_chars.into_iter().collect();
// 如果字母数超过10个,则无解
if chars.len() > 10 {
return None;
}
// 生成 0-9 的所有排列
let digits: Vec<u8> = (0..10).collect();
let permutations = permute(&digits, chars.len());
// 尝试每个排列
for perm in permutations {
let mapping: HashMap<char, u8> = chars.iter().zip(perm.iter()).map(|(&c, &d)| (c, d)).collect();
// 检查首字母是否为0
if first_chars.iter().any(|&c| mapping[c] == 0) {
continue;
}
// 验证等式
if verify_equation(&left_parts, right, &mapping) {
return Some(mapping);
}
}
None
}
// 生成排列的辅助函数
fn permute(digits: &[u8], size: usize) -> Vec<Vec<u8>> {
let mut result = Vec::new();
let mut used = vec![false; digits.len()];
let mut current = Vec::with_capacity(size);
permute_helper(digits, size, &mut used, &mut current, &mut result);
result
}
fn permute_helper(
digits: &[u8],
size: usize,
used: &mut [bool],
current: &mut Vec<u8>,
result: &mut Vec<Vec<u8>>,
) {
if current.len() == size {
result.push(current.clone());
return;
}
for i in 0..digits.len() {
if !used[i] {
used[i] = true;
current.push(digits[i]);
permute_helper(digits, size, used, current, result);
current.pop();
used[i] = false;
}
}
}
// 验证等式的辅助函数
fn verify_equation(left_parts: &[&str], right: &str, mapping: &HashMap<char, u8>) -> bool {
let left_sum: u64 = left_parts
.iter()
.map(|part| word_to_number(part, mapping))
.sum();
let right_value = word_to_number(right, mapping);
left_sum == right_value
}
// 将单词转换为数字
fn word_to_number(word: &str, mapping: &HashMap<char, u8>) -> u64 {
let mut result = 0;
for c in word.chars() {
if let Some(&digit) = mapping.get(&c) {
result = result * 10 + digit as u64;
}
}
result
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
#[test]
fn test_with_three_letters() {
assert_alphametic_solution_eq("I + BB == ILL", &[('I', 1), ('B', 9), ('L', 0)]);
}
最简单的用例,I(1) + BB(99) = ILL(100)。
#[test]
fn test_must_have_unique_value_for_each_letter() {
let answer = alphametics::solve("A == B");
assert_eq!(answer, None);
}
不同字母必须代表不同数字,所以 A==B 无解。
#[test]
fn test_leading_zero_solution_is_invalid() {
let answer = alphametics::solve("ACA + DD == BD");
assert_eq!(answer, None);
}
单词首字母不能为 0,所以 A 和 B 不能为 0。
#[test]
fn test_puzzle_with_eight_letters() {
assert_alphametic_solution_eq(
"SEND + MORE == MONEY",
&[
('S', 9),
('E', 5),
('N', 6),
('D', 7),
('M', 1),
('O', 0),
('R', 8),
('Y', 2),
],
);
}
经典的 SEND + MORE = MONEY 谜题。
性能优化
对于复杂的问题,我们可以考虑以下优化:
- 提前剪枝:在生成排列时就检查首字母不能为0的约束
- 约束传播:利用数学关系减少搜索空间
- 启发式搜索:优先尝试更有可能的数字分配
优化版本:
use std::collections::HashMap;
use std::collections::HashSet;
pub fn solve(input: &str) -> Option<HashMap<char, u8>> {
// 解析输入
let parts: Vec<&str> = input.split("==").collect();
if parts.len() != 2 {
return None;
}
let right = parts[1].trim();
let left_parts: Vec<&str> = parts[0].split('+').map(|s| s.trim()).collect();
// 收集所有唯一字母
let mut unique_chars = HashSet::new();
let mut first_chars = HashSet::new();
for part in &left_parts {
if part.is_empty() {
return None;
}
first_chars.insert(part.chars().next().unwrap());
for c in part.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
}
first_chars.insert(right.chars().next().unwrap());
for c in right.chars() {
if c.is_alphabetic() {
unique_chars.insert(c);
}
}
let chars: Vec<char> = unique_chars.into_iter().collect();
// 如果字母数超过10个,则无解
if chars.len() > 10 {
return None;
}
// 转换为数组以提高性能
let first_char_indices: Vec<usize> = first_chars.iter()
.map(|c| chars.iter().position(|&x| x == *c).unwrap())
.collect();
// 生成 0-9 的所有排列
let digits: Vec<u8> = (0..10).collect();
let permutations = permute(&digits, chars.len());
// 尝试每个排列
for perm in permutations {
// 检查首字母是否为0
if first_char_indices.iter().any(|&i| perm[i] == 0) {
continue;
}
let mapping: HashMap<char, u8> = chars.iter().zip(perm.iter()).map(|(&c, &d)| (c, d)).collect();
// 验证等式
if verify_equation(&left_parts, right, &mapping) {
return Some(mapping);
}
}
None
}
// 生成排列的辅助函数
fn permute(digits: &[u8], size: usize) -> Vec<Vec<u8>> {
let mut result = Vec::new();
let mut used = vec![false; digits.len()];
let mut current = Vec::with_capacity(size);
permute_helper(digits, size, &mut used, &mut current, &mut result);
result
}
fn permute_helper(
digits: &[u8],
size: usize,
used: &mut [bool],
current: &mut Vec<u8>,
result: &mut Vec<Vec<u8>>,
) {
if current.len() == size {
result.push(current.clone());
return;
}
for i in 0..digits.len() {
if !used[i] {
used[i] = true;
current.push(digits[i]);
permute_helper(digits, size, used, current, result);
current.pop();
used[i] = false;
}
}
}
// 验证等式的辅助函数
fn verify_equation(left_parts: &[&str], right: &str, mapping: &HashMap<char, u8>) -> bool {
let left_sum: u64 = left_parts
.iter()
.map(|part| word_to_number(part, mapping))
.sum();
let right_value = word_to_number(right, mapping);
left_sum == right_value
}
// 将单词转换为数字
fn word_to_number(word: &str, mapping: &HashMap<char, u8>) -> u64 {
let mut result = 0;
for c in word.chars() {
if let Some(&digit) = mapping.get(&c) {
result = result * 10 + digit as u64;
}
}
result
}
约束满足问题
字母算术是约束满足问题(Constraint Satisfaction Problem, CSP)的一个典型例子。CSP 包含:
- 变量:每个字母都是一个变量
- 域:每个变量的可能取值(0-9)
- 约束:
- 所有变量取值必须不同
- 首字母不能为 0
- 等式必须成立
实际应用
约束满足问题在现实中有许多应用:
- 排班系统:为员工安排工作时间
- 资源配置:分配有限资源给不同任务
- 电路设计:布局和布线问题
- 人工智能:规划和推理系统
总结
通过 alphametics 练习,我们学到了:
- 约束满足问题:理解了 CSP 的基本概念和解决方法
- 排列生成:学会了如何生成和操作排列组合
- 字符串解析:掌握了复杂字符串的解析技术
- 算法优化:实践了剪枝和提前终止等优化技术
- 数据结构:熟练使用 HashMap 和 HashSet 等集合类型
这些技能在实际开发中非常有用,特别是在处理复杂的业务逻辑和优化计算密集型任务时。约束满足问题虽然看起来抽象,但它是一种强大的建模工具,可以帮助我们解决许多现实世界的问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)