扫雷游戏是微软从1990年代开始在Windows系统中内置的经典游戏之一。在游戏中,玩家需要根据数字提示推理出地雷的位置。在 Exercism 的 "minesweeper" 练习中,我们需要实现扫雷游戏的提示数字生成算法。这不仅能帮助我们掌握二维数组处理和邻域计算,还能深入学习Rust中的字符串处理、边界检查和算法实现。
什么是扫雷提示生成?
扫雷提示生成是扫雷游戏中的核心算法之一。给定一个包含地雷位置的网格,算法需要为每个空格计算周围8个方向内地雷的数量,并用数字标记。如果某个空格周围没有地雷,则保持空白。
规则如下:
- 地雷用'*'表示
- 空格用' '表示
- 数字表示该位置周围8个格子内地雷的数量
- 如果周围没有地雷,保持空白
例如,对于以下输入:
* *
*
* *
输出应该是:
1*3*1
13*31
1*3*1
扫雷提示生成在以下领域有应用:
- 游戏开发:扫雷游戏实现
- 图像处理:邻域操作和卷积运算
- 数据挖掘:邻近点分析
- GIS系统:空间邻域分析
让我们先看看练习提供的函数签名:
rust
pub fn annotate(minefield: &[&str]) -> Vec<String> {
unimplemented!("\nAnnotate each square of the given minefield with the number of mines that surround said square (blank if there are no surrounding mines):\n{:#?}\n", minefield);
}
我们需要实现一个函数,为给定的地雷网格生成提示数字。
设计分析
1. 核心要求
- 二维网格处理:处理字符串切片表示的二维网格
- 邻域计算:计算每个位置周围8个方向的邻居
- 地雷计数:统计邻居中的地雷数量
- 输出格式:生成带提示数字的字符串向量
2. 技术要点
- 边界检查:确保邻居位置在网格范围内
- 字符处理:处理不同类型的字符(地雷、空格、数字)
- 数组索引:正确处理二维数组的行列索引
- 性能优化:避免重复计算
完整实现
1. 基础实现
rust
pub fn annotate(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
let mut result = Vec::with_capacity(rows);
for i in 0..rows {
let mut row_string = String::with_capacity(cols);
for j in 0..cols {
if minefield[i].chars().nth(j).unwrap() == '*' {
row_string.push('*');
} else {
let count = count_mines(minefield, i, j);
if count == 0 {
row_string.push(' ');
} else {
row_string.push_str(&count.to_string());
}
}
}
result.push(row_string);
}
result
}
fn count_mines(minefield: &[&str], row: usize, col: usize) -> u8 {
let rows = minefield.len();
let cols = minefield[0].len();
let mut count = 0;
// 检查周围的8个位置
for i in row.saturating_sub(1)..=row + 1 {
for j in col.saturating_sub(1)..=col + 1 {
// 确保在边界内且不是当前位置
if i < rows && j < cols && (i != row || j != col) {
if minefield[i].chars().nth(j).unwrap() == '*' {
count += 1;
}
}
}
}
count
}
2. 优化实现
rust
pub fn annotate(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
let mut result = Vec::with_capacity(rows);
// 将字符串转换为字符向量以提高访问效率
let char_grid: Vec<Vec<char>> = minefield
.iter()
.map(|row| row.chars().collect())
.collect();
for i in 0..rows {
let mut row_string = String::with_capacity(cols);
for j in 0..cols {
if char_grid[i][j] == '*' {
row_string.push('*');
} else {
let count = count_mines(&char_grid, i, j);
if count == 0 {
row_string.push(' ');
} else {
row_string.push((b'0' + count) as char);
}
}
}
result.push(row_string);
}
result
}
fn count_mines(grid: &[Vec<char>], row: usize, col: usize) -> u8 {
let rows = grid.len();
let cols = grid[0].len();
let mut count = 0;
// 检查周围的8个位置
for i in row.saturating_sub(1)..=usize::min(row + 1, rows - 1) {
for j in col.saturating_sub(1)..=usize::min(col + 1, cols - 1) {
// 确保不是当前位置
if i != row || j != col {
if grid[i][j] == '*' {
count += 1;
}
}
}
}
count
}
3. 函数式实现
rust
pub fn annotate(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
// 将字符串转换为字符向量以提高访问效率
let char_grid: Vec<Vec<char>> = minefield
.iter()
.map(|row| row.chars().collect())
.collect();
(0..rows)
.map(|i| {
(0..cols)
.map(|j| {
if char_grid[i][j] == '*' {
'*'
} else {
let count = count_mines(&char_grid, i, j);
if count == 0 {
' '
} else {
(b'0' + count) as char
}
}
})
.collect()
})
.collect()
}
fn count_mines(grid: &[Vec<char>], row: usize, col: usize) -> u8 {
let rows = grid.len();
let cols = grid[0].len();
(row.saturating_sub(1)..=usize::min(row + 1, rows - 1))
.flat_map(|i| {
(col.saturating_sub(1)..=usize::min(col + 1, cols - 1))
.filter_map(move |j| {
if i != row || j != col {
Some((i, j))
} else {
None
}
})
})
.filter(|&(i, j)| grid[i][j] == '*')
.count() as u8
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
rust
#[test]
fn no_rows() {
#[rustfmt::skip]
run_test(&[
]);
}
空网格应该返回空向量。
rust
#[test]
fn no_columns() {
#[rustfmt::skip]
run_test(&[
"",
]);
}
只有空行的网格应该返回包含空字符串的向量。
rust
#[test]
fn no_mines() {
#[rustfmt::skip]
run_test(&[
" ",
" ",
" ",
]);
}
没有地雷的网格应该全部是空格。
rust
#[test]
fn board_with_only_mines() {
#[rustfmt::skip]
run_test(&[
"***",
"***",
"***",
]);
}
全是地雷的网格应该保持不变。
rust
#[test]
fn mine_surrounded_by_spaces() {
#[rustfmt::skip]
run_test(&[
"111",
"1*1",
"111",
]);
}
地雷周围应该显示数字提示。
rust
#[test]
fn space_surrounded_by_mines() {
#[rustfmt::skip]
run_test(&[
"***",
"*8*",
"***",
]);
}
被地雷包围的空格应该显示8。
rust
#[test]
fn cross() {
#[rustfmt::skip]
run_test(&[
" 2*2 ",
"25*52",
"*****",
"25*52",
" 2*2 ",
]);
}
复杂的交叉模式应该正确计算。
性能优化版本
考虑性能的优化实现:
rust
pub fn annotate(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
// 预分配结果向量
let mut result = Vec::with_capacity(rows);
// 使用字节处理提高性能
let byte_grid: Vec<&[u8]> = minefield.iter().map(|s| s.as_bytes()).collect();
for i in 0..rows {
let mut row_bytes = Vec::with_capacity(cols);
for j in 0..cols {
if byte_grid[i][j] == b'*' {
row_bytes.push(b'*');
} else {
let count = count_mines_bytes(&byte_grid, i, j);
if count == 0 {
row_bytes.push(b' ');
} else {
row_bytes.push(b'0' + count);
}
}
}
// 安全地从字节创建字符串
result.push(String::from_utf8(row_bytes).unwrap());
}
result
}
fn count_mines_bytes(grid: &[&[u8]], row: usize, col: usize) -> u8 {
let rows = grid.len();
let cols = grid[0].len();
let mut count = 0;
let start_row = row.saturating_sub(1);
let end_row = usize::min(row + 1, rows - 1);
let start_col = col.saturating_sub(1);
let end_col = usize::min(col + 1, cols - 1);
for i in start_row..=end_row {
for j in start_col..=end_col {
if i != row || j != col {
if grid[i][j] == b'*' {
count += 1;
}
}
}
}
count
}
// 使用预计算的计数网格版本
pub fn annotate_precalculated(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
// 创建计数网格
let mut count_grid = vec![vec![0u8; cols]; rows];
// 使用字节处理提高性能
let byte_grid: Vec<&[u8]> = minefield.iter().map(|s| s.as_bytes()).collect();
// 首先标记所有地雷并更新邻居计数
for i in 0..rows {
for j in 0..cols {
if byte_grid[i][j] == b'*' {
// 更新周围8个位置的计数
for di in -1..=1 {
for dj in -1..=1 {
if di == 0 && dj == 0 {
continue;
}
let ni = i as i32 + di;
let nj = j as i32 + dj;
if ni >= 0 && ni < rows as i32 && nj >= 0 && nj < cols as i32 {
let ni = ni as usize;
let nj = nj as usize;
if byte_grid[ni][nj] != b'*' {
count_grid[ni][nj] += 1;
}
}
}
}
}
}
}
// 生成结果
(0..rows)
.map(|i| {
(0..cols)
.map(|j| {
if byte_grid[i][j] == b'*' {
'*'
} else {
let count = count_grid[i][j];
if count == 0 {
' '
} else {
(b'0' + count) as char
}
}
})
.collect()
})
.collect()
}
错误处理和边界情况
考虑更多边界情况的实现:
rust
#[derive(Debug, PartialEq)]
pub enum MinesweeperError {
InvalidInput,
InconsistentRowLength,
}
pub fn annotate_safe(minefield: &[&str]) -> Result<Vec<String>, MinesweeperError> {
// 检查输入有效性
if minefield.is_empty() {
return Ok(vec![]);
}
// 检查行长度一致性
let first_row_len = minefield[0].len();
if minefield.iter().any(|row| row.len() != first_row_len) {
return Err(MinesweeperError::InconsistentRowLength);
}
// 检查字符有效性
for row in minefield {
for c in row.chars() {
if c != ' ' && c != '*' {
return Err(MinesweeperError::InvalidInput);
}
}
}
Ok(annotate(minefield))
}
pub fn annotate(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
let char_grid: Vec<Vec<char>> = minefield
.iter()
.map(|row| row.chars().collect())
.collect();
(0..rows)
.map(|i| {
(0..cols)
.map(|j| {
if char_grid[i][j] == '*' {
'*'
} else {
let count = count_mines(&char_grid, i, j);
if count == 0 {
' '
} else {
(b'0' + count) as char
}
}
})
.collect()
})
.collect()
}
fn count_mines(grid: &[Vec<char>], row: usize, col: usize) -> u8 {
let rows = grid.len();
let cols = grid[0].len();
(row.saturating_sub(1)..=usize::min(row + 1, rows - 1))
.flat_map(|i| {
(col.saturating_sub(1)..=usize::min(col + 1, cols - 1))
.filter_map(move |j| {
if i != row || j != col {
Some((i, j))
} else {
None
}
})
})
.filter(|&(i, j)| grid[i][j] == '*')
.count() as u8
}
扩展功能
基于基础实现,我们可以添加更多功能:
rust
pub struct Minesweeper {
grid: Vec<Vec<char>>,
rows: usize,
cols: usize,
}
impl Minesweeper {
pub fn new(minefield: &[&str]) -> Self {
let rows = minefield.len();
let cols = if rows > 0 { minefield[0].len() } else { 0 };
let grid: Vec<Vec<char>> = minefield
.iter()
.map(|row| row.chars().collect())
.collect();
Minesweeper { grid, rows, cols }
}
pub fn annotate(&self) -> Vec<String> {
if self.rows == 0 {
return vec![];
}
(0..self.rows)
.map(|i| {
(0..self.cols)
.map(|j| {
if self.grid[i][j] == '*' {
'*'
} else {
let count = self.count_mines(i, j);
if count == 0 {
' '
} else {
(b'0' + count) as char
}
}
})
.collect()
})
.collect()
}
fn count_mines(&self, row: usize, col: usize) -> u8 {
(row.saturating_sub(1)..=usize::min(row + 1, self.rows - 1))
.flat_map(|i| {
(col.saturating_sub(1)..=usize::min(col + 1, self.cols - 1))
.filter_map(move |j| {
if i != row || j != col {
Some((i, j))
} else {
None
}
})
})
.filter(|&(i, j)| self.grid[i][j] == '*')
.count() as u8
}
// 获取特定位置的值
pub fn get_cell(&self, row: usize, col: usize) -> Option<char> {
if row < self.rows && col < self.cols {
Some(self.grid[row][col])
} else {
None
}
}
// 获取周围邻居的信息
pub fn get_neighbors(&self, row: usize, col: usize) -> Vec<(usize, usize, char)> {
(row.saturating_sub(1)..=usize::min(row + 1, self.rows - 1))
.flat_map(|i| {
(col.saturating_sub(1)..=usize::min(col + 1, self.cols - 1))
.filter_map(move |j| {
if i != row || j != col {
Some((i, j, self.grid[i][j]))
} else {
None
}
})
})
.collect()
}
// 计算整个网格的地雷总数
pub fn count_total_mines(&self) -> usize {
self.grid
.iter()
.flat_map(|row| row.iter())
.filter(|&&c| c == '*')
.count()
}
// 验证网格是否有效
pub fn is_valid(&self) -> bool {
if self.rows == 0 {
return true;
}
let first_row_len = self.cols;
self.grid.iter().all(|row| row.len() == first_row_len)
}
}
// 便利函数
pub fn annotate(minefield: &[&str]) -> Vec<String> {
Minesweeper::new(minefield).annotate()
}
// 支持不同提示样式的版本
pub struct MinesweeperConfig {
pub mine_char: char,
pub empty_char: char,
pub number_style: NumberStyle,
}
#[derive(Clone)]
pub enum NumberStyle {
Digits, // 使用数字 1-8
Letters, // 使用字母 a-h
Roman, // 使用罗马数字 I-VIII
}
impl Minesweeper {
pub fn annotate_with_config(&self, config: &MinesweeperConfig) -> Vec<String> {
if self.rows == 0 {
return vec![];
}
(0..self.rows)
.map(|i| {
(0..self.cols)
.map(|j| {
if self.grid[i][j] == '*' {
config.mine_char
} else {
let count = self.count_mines(i, j);
if count == 0 {
config.empty_char
} else {
match config.number_style {
NumberStyle::Digits => (b'0' + count) as char,
NumberStyle::Letters => (b'a' + count - 1) as char,
NumberStyle::Roman => match count {
1 => 'I',
2 => 'I',
3 => 'I',
4 => 'I',
5 => 'V',
6 => 'V',
7 => 'V',
8 => 'V',
_ => ' ',
},
// 简化实现
}
}
}
})
.collect()
})
.collect()
}
}
实际应用场景
扫雷提示生成在实际开发中有以下应用:
- 游戏开发:扫雷游戏和其他益智游戏
- 图像处理:邻域操作和卷积运算
- 数据挖掘:邻近点分析和聚类算法
- GIS系统:空间邻域分析和热点检测
- 计算机视觉:边缘检测和特征提取
- 科学计算:有限元分析和网格处理
- 社交网络:社交关系分析和推荐系统
- 生物信息学:基因序列分析和蛋白质结构预测
算法复杂度分析
-
时间复杂度:O(rows × cols × 8) = O(rows × cols)
- 需要遍历网格中的每个位置,每个位置最多检查8个邻居
-
空间复杂度:O(rows × cols)
- 需要存储结果网格和可能的中间表示
与其他实现方式的比较
rust
// 使用递归的实现
pub fn annotate_recursive(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
let char_grid: Vec<Vec<char>> = minefield
.iter()
.map(|row| row.chars().collect())
.collect();
fn process_cell(
grid: &[Vec<char>],
result: &mut Vec<Vec<char>>,
row: usize,
col: usize,
rows: usize,
cols: usize,
) {
if row >= rows || col >= cols {
return;
}
if grid[row][col] == '*' {
result[row][col] = '*';
} else {
let count = count_mines(grid, row, col);
result[row][col] = if count == 0 { ' ' } else { (b'0' + count) as char };
}
}
fn count_mines(grid: &[Vec<char>], row: usize, col: usize) -> u8 {
let rows = grid.len();
let cols = grid[0].len();
(row.saturating_sub(1)..=usize::min(row + 1, rows - 1))
.flat_map(|i| {
(col.saturating_sub(1)..=usize::min(col + 1, cols - 1))
.filter_map(move |j| {
if i != row || j != col {
Some((i, j))
} else {
None
}
})
})
.filter(|&(i, j)| grid[i][j] == '*')
.count() as u8
}
let mut result: Vec<Vec<char>> = vec![vec![' '; cols]; rows];
for i in 0..rows {
for j in 0..cols {
process_cell(&char_grid, &mut result, i, j, rows, cols);
}
}
result.into_iter().map(|row| row.into_iter().collect()).collect()
}
// 使用迭代器链的函数式实现
pub fn annotate_functional(minefield: &[&str]) -> Vec<String> {
minefield
.iter()
.enumerate()
.map(|(i, row)| {
row.chars()
.enumerate()
.map(|(j, cell)| {
if cell == '*' {
'*'
} else {
let count = minefield
.iter()
.enumerate()
.flat_map(|(di, d_row)| {
d_row.chars().enumerate().filter_map(move |(dj, d_cell)| {
// 检查是否在周围8个位置内且不是当前位置
if (di as i32 - i as i32).abs() <= 1
&& (dj as i32 - j as i32).abs() <= 1
&& (di != i || dj != j)
&& d_cell == '*'
{
Some(())
} else {
None
}
})
})
.count();
if count == 0 {
' '
} else {
(b'0' + count as u8) as char
}
}
})
.collect()
})
.collect()
}
// 使用第三方库的实现
// [dependencies]
// ndarray = "0.15"
use ndarray::Array2;
pub fn annotate_ndarray(minefield: &[&str]) -> Vec<String> {
if minefield.is_empty() {
return vec![];
}
let rows = minefield.len();
let cols = minefield[0].len();
// 创建二维数组
let mut grid = Array2::<char>::default((rows, cols));
for (i, row) in minefield.iter().enumerate() {
for (j, ch) in row.chars().enumerate() {
grid[[i, j]] = ch;
}
}
// 创建计数数组
let mut counts = Array2::<u8>::zeros((rows, cols));
// 计算每个位置的地雷数
for i in 0..rows {
for j in 0..cols {
if grid[[i, j]] == '*' {
counts[[i, j]] = 0; // 地雷位置保持为0或用特殊标记
} else {
let count = (i.saturating_sub(1)..=usize::min(i + 1, rows - 1))
.flat_map(|ni| {
(j.saturating_sub(1)..=usize::min(j + 1, cols - 1)).map(move |nj| (ni, nj))
})
.filter(|&(ni, nj)| (ni != i || nj != j) && grid[[ni, nj]] == '*')
.count() as u8;
counts[[i, j]] = count;
}
}
}
// 生成结果
(0..rows)
.map(|i| {
(0..cols)
.map(|j| {
if grid[[i, j]] == '*' {
'*'
} else {
let count = counts[[i, j]];
if count == 0 {
' '
} else {
(b'0' + count) as char
}
}
})
.collect()
})
.collect()
}
总结
通过 minesweeper 练习,我们学到了:
- 二维数组处理:掌握了二维网格的表示和处理方法
- 邻域计算:学会了计算网格中每个位置的邻居
- 边界检查:理解了如何安全地处理数组边界
- 性能优化:了解了不同实现方式的性能特点
- 字符处理:深入理解了字符和字节级别的处理
- 算法设计:学会了设计网格处理算法
这些技能在实际开发中非常有用,特别是在游戏开发、图像处理、数据挖掘等场景中。扫雷提示生成虽然是一个具体的游戏算法问题,但它涉及到了二维数组处理、邻域计算、边界处理等许多核心概念,是学习Rust实用编程的良好起点。
通过这个练习,我们也看到了Rust在处理二维数据和字符数据方面的强大能力,以及如何用安全且高效的方式实现经典算法。这种结合了安全性和性能的语言特性正是Rust的魅力所在。