Rust 练习册 :Minesweeper与二维数组处理

扫雷游戏是微软从1990年代开始在Windows系统中内置的经典游戏之一。在游戏中,玩家需要根据数字提示推理出地雷的位置。在 Exercism 的 "minesweeper" 练习中,我们需要实现扫雷游戏的提示数字生成算法。这不仅能帮助我们掌握二维数组处理和邻域计算,还能深入学习Rust中的字符串处理、边界检查和算法实现。

什么是扫雷提示生成?

扫雷提示生成是扫雷游戏中的核心算法之一。给定一个包含地雷位置的网格,算法需要为每个空格计算周围8个方向内地雷的数量,并用数字标记。如果某个空格周围没有地雷,则保持空白。

规则如下:

  1. 地雷用'*'表示
  2. 空格用' '表示
  3. 数字表示该位置周围8个格子内地雷的数量
  4. 如果周围没有地雷,保持空白

例如,对于以下输入:

复制代码
 * * 
  *  
 * * 

输出应该是:

复制代码
1*3*1
13*31
1*3*1

扫雷提示生成在以下领域有应用:

  1. 游戏开发:扫雷游戏实现
  2. 图像处理:邻域操作和卷积运算
  3. 数据挖掘:邻近点分析
  4. 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. 核心要求

  1. 二维网格处理:处理字符串切片表示的二维网格
  2. 邻域计算:计算每个位置周围8个方向的邻居
  3. 地雷计数:统计邻居中的地雷数量
  4. 输出格式:生成带提示数字的字符串向量

2. 技术要点

  1. 边界检查:确保邻居位置在网格范围内
  2. 字符处理:处理不同类型的字符(地雷、空格、数字)
  3. 数组索引:正确处理二维数组的行列索引
  4. 性能优化:避免重复计算

完整实现

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()
    }
}

实际应用场景

扫雷提示生成在实际开发中有以下应用:

  1. 游戏开发:扫雷游戏和其他益智游戏
  2. 图像处理:邻域操作和卷积运算
  3. 数据挖掘:邻近点分析和聚类算法
  4. GIS系统:空间邻域分析和热点检测
  5. 计算机视觉:边缘检测和特征提取
  6. 科学计算:有限元分析和网格处理
  7. 社交网络:社交关系分析和推荐系统
  8. 生物信息学:基因序列分析和蛋白质结构预测

算法复杂度分析

  1. 时间复杂度:O(rows × cols × 8) = O(rows × cols)

    • 需要遍历网格中的每个位置,每个位置最多检查8个邻居
  2. 空间复杂度: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 练习,我们学到了:

  1. 二维数组处理:掌握了二维网格的表示和处理方法
  2. 邻域计算:学会了计算网格中每个位置的邻居
  3. 边界检查:理解了如何安全地处理数组边界
  4. 性能优化:了解了不同实现方式的性能特点
  5. 字符处理:深入理解了字符和字节级别的处理
  6. 算法设计:学会了设计网格处理算法

这些技能在实际开发中非常有用,特别是在游戏开发、图像处理、数据挖掘等场景中。扫雷提示生成虽然是一个具体的游戏算法问题,但它涉及到了二维数组处理、邻域计算、边界处理等许多核心概念,是学习Rust实用编程的良好起点。

通过这个练习,我们也看到了Rust在处理二维数据和字符数据方面的强大能力,以及如何用安全且高效的方式实现经典算法。这种结合了安全性和性能的语言特性正是Rust的魅力所在。

相关推荐
小蒜学长2 小时前
springboot基于Java的校园导航微信小程序的设计与实现(代码+数据库+LW)
java·spring boot·后端·微信小程序
微学AI3 小时前
基于openEuler操作系统的Docker部署与AI应用实践操作与研究
后端
王元_SmallA3 小时前
IDEA + Spring Boot 的三种热加载方案
java·后端
LCG元3 小时前
实战:用 Shell 脚本自动备份网站和数据库,并上传到云存储
后端
Yeats_Liao3 小时前
时序数据库系列(四):InfluxQL查询语言详解
数据库·后端·sql·时序数据库
清空mega3 小时前
从零开始搭建 flask 博客实验(常见疑问)
后端·python·flask
白衣鸽子3 小时前
MySQL数据库的“隐形杀手”:深入理解文件结构与治理数据碎片
数据库·后端·mysql
neoooo3 小时前
⚙️ Spring Boot × @RequiredArgsConstructor:写出最干净的依赖注入代码
spring boot·后端·spring
开发者小天3 小时前
React中的useRef的用法
开发语言·前端·javascript·react.js