在经典的多米诺骨牌游戏中,玩家需要将骨牌按一定规则连接起来形成链条。每张骨牌有两个数字,玩家需要将相同数字的面相邻放置。在 Exercism 的 "dominoes" 练习中,我们需要解决一个更具挑战性的问题:给定一组多米诺骨牌,判断是否能将它们排列成一个闭合的环形链条。这不仅能帮助我们掌握回溯算法和图论知识,还能深入学习 Rust 中的递归和状态搜索技巧。
什么是多米诺骨牌问题?
多米诺骨牌问题要求我们判断给定的一组骨牌是否能排列成一个闭合的环形链条,满足以下条件:
- 连续性:相邻骨牌的相邻数字必须相同
- 完整性:所有输入的骨牌都必须使用一次且仅一次
- 闭合性:链条的首尾也必须连接(形成环形)
例如,对于骨牌 [(1,2), (2,3), (3,1)],可以排列成:
(1,2) -> (2,3) -> (3,1) -> (回到起点)
让我们先看看练习提供的函数签名:
rust
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
unimplemented!("From the given input '{:?}' construct a proper dominoes chain or return None if it is not possible.", input);
}
我们需要实现这个函数,它应该:
- 接收一个骨牌数组作为输入
- 如果能形成闭合链条,返回排列后的骨牌序列
- 如果不能形成闭合链条,返回 None
算法分析
1. 问题建模
这个问题可以看作是图论中的欧拉回路问题:
- 每个数字代表图中的一个节点
- 每张骨牌代表图中的一条边
- 寻找一条经过所有边恰好一次的闭合路径
2. 解决思路
我们可以使用回溯算法来解决这个问题:
- 尝试以每张骨牌作为起点
- 递归地尝试添加下一张匹配的骨牌
- 如果所有骨牌都已使用且能闭合,则找到解
- 如果当前路径无法继续,回溯并尝试其他可能性
基础实现
1. 回溯算法实现
rust
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 处理空输入的特殊情况
if input.is_empty() {
return Some(vec![]);
}
// 复制输入以便进行修改
let mut dominoes = input.to_vec();
let mut chain = Vec::with_capacity(dominoes.len());
// 尝试每张骨牌作为起点
for i in 0..dominoes.len() {
let domino = dominoes.remove(i);
// 尝试不翻转和翻转两种情况
chain.push(domino);
if build_chain(&mut dominoes, &mut chain) {
return Some(chain);
}
chain.clear();
dominoes.insert(i, domino);
// 尝试翻转骨牌作为起点
let flipped_domino = (domino.1, domino.0);
chain.push(flipped_domino);
if build_chain(&mut dominoes, &mut chain) {
return Some(chain);
}
chain.clear();
dominoes[i] = domino; // 恢复原状
}
None
}
fn build_chain(dominoes: &mut Vec<(u8, u8)>, chain: &mut Vec<(u8, u8)>) -> bool {
// 如果所有骨牌都已使用,检查是否能闭合
if dominoes.is_empty() {
return chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
});
}
// 获取当前链条末端的数字
let target = chain.last().unwrap().1;
// 尝试每张剩余的骨牌
for i in 0..dominoes.len() {
let domino = dominoes.remove(i);
// 尝试不翻转
if domino.0 == target {
chain.push(domino);
if build_chain(dominoes, chain) {
return true;
}
chain.pop();
}
// 尝试翻转
if domino.1 == target {
let flipped_domino = (domino.1, domino.0);
chain.push(flipped_domino);
if build_chain(dominoes, chain) {
return true;
}
chain.pop();
}
// 恢复骨牌
dominoes.insert(i, domino);
}
false
}
2. 优化实现
rust
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 处理特殊情况
if input.is_empty() {
return Some(vec![]);
}
// 使用索引而不是移动元素来提高效率
let dominoes = input.to_vec();
let mut used = vec![false; dominoes.len()];
let mut chain = Vec::with_capacity(dominoes.len());
// 尝试每张骨牌作为起点
for i in 0..dominoes.len() {
// 尝试不翻转
used[i] = true;
chain.push(dominoes[i]);
if build_chain_optimized(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
// 尝试翻转
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain_optimized(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
}
None
}
fn build_chain_optimized(
dominoes: &[(u8, u8)],
used: &mut [bool],
chain: &mut Vec<(u8, u8)>,
) -> bool {
// 如果所有骨牌都已使用,检查是否能闭合
if chain.len() == dominoes.len() {
return chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
});
}
// 获取当前链条末端的数字
let target = chain.last().unwrap().1;
// 尝试每张未使用的骨牌
for i in 0..dominoes.len() {
if used[i] {
continue;
}
// 尝试不翻转
if dominoes[i].0 == target {
used[i] = true;
chain.push(dominoes[i]);
if build_chain_optimized(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
// 尝试翻转
if dominoes[i].1 == target {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain_optimized(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
}
false
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
rust
#[test]
fn empty_input_empty_output() {
let input = &[];
assert_eq!(dominoes::chain(input), Some(vec![]));
}
空输入应该返回空链条。
rust
#[test]
fn singleton_input_singleton_output() {
let input = &[(1, 1)];
assert_correct(input);
}
单张骨牌如果两个数字相同,可以形成闭合链条。
rust
#[test]
fn singleton_that_cant_be_chained() {
let input = &[(1, 2)];
assert_eq!(dominoes::chain(input), None);
}
单张骨牌如果两个数字不同,无法形成闭合链条。
rust
#[test]
fn no_repeat_numbers() {
let input = &[(1, 2), (3, 1), (2, 3)];
assert_correct(input);
}
多张骨牌可以重新排列形成闭合链条。
rust
#[test]
fn no_chains() {
let input = &[(1, 2), (4, 1), (2, 3)];
assert_eq!(dominoes::chain(input), None);
}
某些骨牌组合无法形成闭合链条。
rust
#[test]
fn need_backtrack() {
let input = &[(1, 2), (2, 3), (3, 1), (2, 4), (2, 4)];
assert_correct(input);
}
有些情况需要回溯才能找到正确解。
完整实现
考虑所有边界情况的完整实现:
rust
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 处理空输入
if input.is_empty() {
return Some(vec![]);
}
// 特殊情况:单张骨牌
if input.len() == 1 {
let domino = input[0];
return if domino.0 == domino.1 {
Some(vec![domino])
} else {
None
};
}
// 使用索引而不是移动元素来提高效率
let dominoes = input.to_vec();
let mut used = vec![false; dominoes.len()];
let mut chain = Vec::with_capacity(dominoes.len());
// 尝试每张骨牌作为起点
for i in 0..dominoes.len() {
// 尝试不翻转
used[i] = true;
chain.push(dominoes[i]);
if build_chain(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
// 尝试翻转(仅当两个数字不同时)
if dominoes[i].0 != dominoes[i].1 {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
}
}
None
}
fn build_chain(
dominoes: &[(u8, u8)],
used: &mut [bool],
chain: &mut Vec<(u8, u8)>,
) -> bool {
// 如果所有骨牌都已使用,检查是否能闭合
if chain.len() == dominoes.len() {
return chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
});
}
// 获取当前链条末端的数字
let target = chain.last().unwrap().1;
// 尝试每张未使用的骨牌
for i in 0..dominoes.len() {
if used[i] {
continue;
}
// 尝试不翻转
if dominoes[i].0 == target {
used[i] = true;
chain.push(dominoes[i]);
if build_chain(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
// 尝试翻转
if dominoes[i].1 == target {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
}
false
}
性能优化版本
考虑性能的优化实现:
rust
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
if input.is_empty() {
return Some(vec![]);
}
if input.len() == 1 {
let domino = input[0];
return if domino.0 == domino.1 {
Some(vec![domino])
} else {
None
};
}
// 预先检查欧拉回路的必要条件
if !has_eulerian_cycle(input) {
return None;
}
let dominoes = input.to_vec();
let mut used = vec![false; dominoes.len()];
let mut chain = Vec::with_capacity(dominoes.len());
// 尝试每张骨牌作为起点
for i in 0..dominoes.len() {
used[i] = true;
chain.push(dominoes[i]);
if build_chain_optimized(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
if dominoes[i].0 != dominoes[i].1 {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain_optimized(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
}
}
None
}
fn has_eulerian_cycle(dominoes: &[(u8, u8)]) -> bool {
use std::collections::HashMap;
// 计算每个数字的度数
let mut degree: HashMap<u8, u32> = HashMap::new();
for &(a, b) in dominoes {
*degree.entry(a).or_insert(0) += 1;
*degree.entry(b).or_insert(0) += 1;
}
// 在欧拉回路中,每个顶点的度数都必须是偶数
degree.values().all(|&d| d % 2 == 0)
}
fn build_chain_optimized(
dominoes: &[(u8, u8)],
used: &mut [bool],
chain: &mut Vec<(u8, u8)>,
) -> bool {
if chain.len() == dominoes.len() {
return chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
});
}
let target = chain.last().unwrap().1;
// 优先尝试匹配的骨牌以提高效率
for i in 0..dominoes.len() {
if used[i] {
continue;
}
if dominoes[i].0 == target {
used[i] = true;
chain.push(dominoes[i]);
if build_chain_optimized(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
if dominoes[i].1 == target {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain_optimized(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
}
false
}
错误处理和边界情况
考虑更多边界情况的实现:
rust
#[derive(Debug, PartialEq)]
pub enum DominoError {
InvalidInput,
NoSolution,
}
pub fn chain_safe(input: &[(u8, u8)]) -> Result<Option<Vec<(u8, u8)>>, DominoError> {
// 验证输入
if input.len() > 100 {
return Err(DominoError::InvalidInput); // 防止过大的输入导致性能问题
}
// 检查数字范围
for &(a, b) in input {
if a > 6 || b > 6 {
// 标准多米诺骨牌数字范围是0-6
return Err(DominoError::InvalidInput);
}
}
Ok(chain(input))
}
pub fn chain(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
if input.is_empty() {
return Some(vec![]);
}
if input.len() == 1 {
let domino = input[0];
return if domino.0 == domino.1 {
Some(vec![domino])
} else {
None
};
}
let dominoes = input.to_vec();
let mut used = vec![false; dominoes.len()];
let mut chain = Vec::with_capacity(dominoes.len());
for i in 0..dominoes.len() {
used[i] = true;
chain.push(dominoes[i]);
if build_chain(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
if dominoes[i].0 != dominoes[i].1 {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain(&dominoes, &mut used, &mut chain) {
return Some(chain);
}
chain.pop();
used[i] = false;
}
}
None
}
fn build_chain(
dominoes: &[(u8, u8)],
used: &mut [bool],
chain: &mut Vec<(u8, u8)>,
) -> bool {
if chain.len() == dominoes.len() {
return chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
});
}
let target = chain.last().unwrap().1;
for i in 0..dominoes.len() {
if used[i] {
continue;
}
if dominoes[i].0 == target {
used[i] = true;
chain.push(dominoes[i]);
if build_chain(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
if dominoes[i].1 == target {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
if build_chain(dominoes, used, chain) {
return true;
}
chain.pop();
used[i] = false;
}
}
false
}
扩展功能
基于基础实现,我们可以添加更多功能:
rust
pub struct DominoSolver;
impl DominoSolver {
pub fn new() -> Self {
DominoSolver
}
pub fn solve(&self, input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
chain(input)
}
// 查找所有可能的解
pub fn find_all_solutions(&self, input: &[(u8, u8)]) -> Vec<Vec<(u8, u8)>> {
if input.is_empty() {
return vec![vec![]];
}
let mut solutions = Vec::new();
let dominoes = input.to_vec();
let mut used = vec![false; dominoes.len()];
let mut chain = Vec::with_capacity(dominoes.len());
for i in 0..dominoes.len() {
used[i] = true;
chain.push(dominoes[i]);
self.find_all_solutions_recursive(&dominoes, &mut used, &mut chain, &mut solutions);
chain.pop();
used[i] = false;
if dominoes[i].0 != dominoes[i].1 {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
self.find_all_solutions_recursive(&dominoes, &mut used, &mut chain, &mut solutions);
chain.pop();
used[i] = false;
}
}
solutions
}
fn find_all_solutions_recursive(
&self,
dominoes: &[(u8, u8)],
used: &mut [bool],
chain: &mut Vec<(u8, u8)>,
solutions: &mut Vec<Vec<(u8, u8)>>,
) {
if chain.len() == dominoes.len() {
if chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
}) {
solutions.push(chain.clone());
}
return;
}
let target = chain.last().unwrap().1;
for i in 0..dominoes.len() {
if used[i] {
continue;
}
if dominoes[i].0 == target {
used[i] = true;
chain.push(dominoes[i]);
self.find_all_solutions_recursive(dominoes, used, chain, solutions);
chain.pop();
used[i] = false;
}
if dominoes[i].1 == target {
used[i] = true;
chain.push((dominoes[i].1, dominoes[i].0));
self.find_all_solutions_recursive(dominoes, used, chain, solutions);
chain.pop();
used[i] = false;
}
}
}
// 检查是否存在解(不返回具体解)
pub fn has_solution(&self, input: &[(u8, u8)]) -> bool {
self.solve(input).is_some()
}
// 验证给定的链条是否有效
pub fn validate_chain(&self, chain: &[(u8, u8)]) -> bool {
if chain.is_empty() {
return true;
}
// 检查连续性
for i in 0..chain.len() - 1 {
if chain[i].1 != chain[i + 1].0 {
return false;
}
}
// 检查闭合性
chain.first().map_or(false, |first| {
chain.last().map_or(false, |last| last.1 == first.0)
})
}
}
实际应用场景
多米诺骨牌问题在实际开发中有以下应用:
- 路径规划:在图中寻找经过所有边的路径
- 游戏开发:多米诺骨牌游戏的AI实现
- 电路设计:在电路板布线中寻找最优路径
- 物流优化:在配送路线规划中确保覆盖所有站点
- 网络路由:在计算机网络中寻找遍历所有链路的路径
- DNA测序:在生物信息学中组装DNA片段
算法复杂度分析
-
时间复杂度:O(N! × 2^N)
- N张骨牌有N!种排列方式
- 每张骨牌可以翻转或不翻转,有2^N种组合
- 实际上由于剪枝会更快
-
空间复杂度:O(N)
- 递归调用栈深度最多为N
- 存储当前链条需要O(N)空间
与其他实现方式的比较
rust
// 使用图论库实现
use petgraph::graph::UnGraph;
use petgraph::algo::is_eulerian;
pub fn chain_graph(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 构建图
let mut graph = UnGraph::<u8, (u8, u8)>::new_undirected();
let mut nodes = std::collections::HashMap::new();
// 添加节点和边
for &(a, b) in input {
let node_a = *nodes.entry(a).or_insert_with(|| graph.add_node(a));
let node_b = *nodes.entry(b).or_insert_with(|| graph.add_node(b));
graph.add_edge(node_a, node_b, (a, b));
}
// 检查是否存在欧拉回路
if is_eulerian(&graph) {
// 实际构造欧拉回路
// ... 这里需要更复杂的实现
unimplemented!()
} else {
None
}
}
// 使用动态规划实现(状态压缩)
pub fn chain_dp(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 使用位掩码表示已使用的骨牌
// 使用状态 (mask, last_value) 表示当前状态
// ... 复杂的DP实现
unimplemented!()
}
// 使用迭代加深搜索实现
pub fn chain_ids(input: &[(u8, u8)]) -> Option<Vec<(u8, u8)>> {
// 逐步增加搜索深度
// ... 迭代加深搜索实现
unimplemented!()
}
总结
通过 dominoes 练习,我们学到了:
- 回溯算法:掌握了经典的回溯算法设计思想
- 图论应用:理解了欧拉回路在实际问题中的应用
- 递归实现:熟练使用递归解决复杂搜索问题
- 状态管理:学会了在递归过程中管理状态和回溯
- 性能优化:了解了剪枝和提前终止等优化技巧
- 边界处理:学会了处理各种边界情况
这些技能在实际开发中非常有用,特别是在解决路径规划、游戏AI、组合优化等问题时。多米诺骨牌问题虽然是一个游戏问题,但它涉及到了回溯算法、图论和状态搜索等许多核心概念,是学习 Rust 算法实现的良好起点。
通过这个练习,我们也看到了 Rust 在递归算法和状态管理方面的强大能力,以及如何用安全且高效的方式实现复杂的搜索算法。这种结合了安全性和性能的语言特性正是 Rust 的魅力所在。