Rust 练习册 80:Grains与位运算

在计算机科学中,位运算是最基础且高效的运算方式之一。在 Exercism 的 "grains" 练习中,我们需要解决一个经典的数学问题:棋盘上的麦粒。这个问题源于一个古老的传说,展示了指数增长的惊人力量。通过这个练习,我们不仅能学习到位运算的使用,还能深入理解Rust中的错误处理和数学计算。

什么是Grains问题?

Grains问题基于一个古老的印度传说:国王要奖励发明国际象棋的智者,智者请求在棋盘的第一格放1粒麦子,第二格放2粒,第三格放4粒,以此类推,每一格都是前一格的两倍。国王欣然同意,但很快就发现全国的麦子都不够支付。

这个问题涉及两个计算:

  1. 计算棋盘上某个格子的麦粒数:2^(n-1)
  2. 计算整个棋盘的麦粒总数:2^64 - 1

让我们先看看练习提供的函数签名:

rust 复制代码
pub fn square(s: u32) -> u64 {
    match s {
        1..=64 => 1 << s - 1,
        _ => panic!("Square must be between 1 and 64"),
    }
}

pub fn total() -> u64 {
    // (1..65).map(square).sum()
    !0
}

这个实现使用了位运算来高效地计算结果。1 << s - 1 表示将数字1左移(s-1)位,这等价于计算2^(s-1)。!0 表示对0进行按位取反,得到u64类型的最大值,即2^64 - 1。

设计分析

1. 核心组件

  1. square函数:计算特定格子的麦粒数
  2. total函数:计算所有格子的麦粒总数
  3. 错误处理:处理无效输入(格子数超出1-64范围)

2. 技术要点

  1. 位运算:使用左移运算符高效计算2的幂
  2. 模式匹配:使用match处理有效和无效输入
  3. panic宏:处理错误情况
  4. 按位取反:使用!0获取最大值

完整实现

1. 基础实现

rust 复制代码
pub fn square(s: u32) -> u64 {
    match s {
        1..=64 => 1 << (s - 1),
        _ => panic!("Square must be between 1 and 64"),
    }
}

pub fn total() -> u64 {
    !0
}

2. 使用Result的错误处理实现

rust 复制代码
#[derive(Debug, PartialEq)]
pub enum GrainError {
    InvalidSquare,
}

pub fn square(s: u32) -> Result<u64, GrainError> {
    if s >= 1 && s <= 64 {
        Ok(1 << (s - 1))
    } else {
        Err(GrainError::InvalidSquare)
    }
}

pub fn total() -> u64 {
    !0
}

3. 使用u128避免溢出的实现

rust 复制代码
pub fn square(s: u32) -> u64 {
    match s {
        1..=64 => 1 << (s - 1),
        _ => panic!("Square must be between 1 and 64"),
    }
}

pub fn total() -> u64 {
    (1u128 << 64 - 1) as u64
}

测试用例分析

通过查看测试用例,我们可以更好地理解需求:

rust 复制代码
fn process_square_case(input: u32, expected: u64) {
    assert_eq!(grains::square(input), expected);
}

#[test]
/// 1
fn test_1() {
    process_square_case(1, 1);
}

第1格应该有1粒麦子(2^0 = 1)。

rust 复制代码
#[test]
/// 2
fn test_2() {
    process_square_case(2, 2);
}

第2格应该有2粒麦子(2^1 = 2)。

rust 复制代码
#[test]
/// 3
fn test_3() {
    process_square_case(3, 4);
}

第3格应该有4粒麦子(2^2 = 4)。

rust 复制代码
#[test]
/// 4
fn test_4() {
    process_square_case(4, 8);
}

第4格应该有8粒麦子(2^3 = 8)。

rust 复制代码
#[test]
/// 16
fn test_16() {
    process_square_case(16, 32_768);
}

第16格应该有32,768粒麦子(2^15 = 32,768)。

rust 复制代码
#[test]
/// 32
fn test_32() {
    process_square_case(32, 2_147_483_648);
}

第32格应该有2,147,483,648粒麦子(2^31 = 2,147,483,648)。

rust 复制代码
#[test]
/// 64
fn test_64() {
    process_square_case(64, 9_223_372_036_854_775_808);
}

第64格应该有9,223,372,036,854,775,808粒麦子(2^63)。

rust 复制代码
#[test]
#[should_panic(expected = "Square must be between 1 and 64")]
fn test_square_0_raises_an_exception() {
    grains::square(0);
}

输入0应该引发panic。

rust 复制代码
#[test]
#[should_panic(expected = "Square must be between 1 and 64")]
fn test_square_greater_than_64_raises_an_exception() {
    grains::square(65);
}

输入大于64应该引发panic。

rust 复制代码
#[test]
fn test_returns_the_total_number_of_grains_on_the_board() {
    assert_eq!(grains::total(), 18_446_744_073_709_551_615);
}

总数应该是18,446,744,073,709,551,615(2^64 - 1)。

性能优化版本

考虑性能的优化实现:

rust 复制代码
pub fn square(s: u32) -> u64 {
    // 使用断言而不是panic,可以在编译时优化掉
    assert!(s >= 1 && s <= 64, "Square must be between 1 and 64");
    1 << (s - 1)
}

pub fn total() -> u64 {
    u64::MAX
}

// 使用const fn在编译时计算
pub const fn square_const(s: u32) -> u64 {
    if s >= 1 && s <= 64 {
        1 << (s - 1)
    } else {
        panic!("Square must be between 1 and 64")
    }
}

pub const fn total_const() -> u64 {
    u64::MAX
}

错误处理和边界情况

考虑更多边界情况的实现:

rust 复制代码
#[derive(Debug, PartialEq)]
pub enum GrainError {
    InvalidSquare,
}

pub fn square(s: u32) -> Result<u64, GrainError> {
    if s >= 1 && s <= 64 {
        Ok(1 << (s - 1))
    } else {
        Err(GrainError::InvalidSquare)
    }
}

pub fn total() -> u64 {
    !0
}

// 带详细错误信息的版本
#[derive(Debug, PartialEq)]
pub enum DetailedGrainError {
    TooLow(u32),
    TooHigh(u32),
}

pub fn square_detailed(s: u32) -> Result<u64, DetailedGrainError> {
    match s {
        1..=64 => Ok(1 << (s - 1)),
        0 => Err(DetailedGrainError::TooLow(s)),
        _ => Err(DetailedGrainError::TooHigh(s)),
    }
}

// 安全版本,处理可能的溢出
pub fn square_safe(s: u32) -> Option<u64> {
    if s >= 1 && s <= 64 {
        Some(1u64.checked_shl(s - 1)?)
    } else {
        None
    }
}

扩展功能

基于基础实现,我们可以添加更多功能:

rust 复制代码
#[derive(Debug, PartialEq)]
pub enum GrainError {
    InvalidSquare,
}

pub struct ChessBoard;

impl ChessBoard {
    pub fn new() -> Self {
        ChessBoard
    }
    
    // 计算特定格子的麦粒数
    pub fn grains_on_square(&self, square: u32) -> Result<u64, GrainError> {
        if square >= 1 && square <= 64 {
            Ok(1 << (square - 1))
        } else {
            Err(GrainError::InvalidSquare)
        }
    }
    
    // 计算所有格子的麦粒总数
    pub fn total_grains(&self) -> u64 {
        !0
    }
    
    // 计算前n个格子的麦粒总数
    pub fn grains_on_first_n_squares(&self, n: u32) -> Result<u64, GrainError> {
        if n > 64 {
            return Err(GrainError::InvalidSquare);
        }
        
        if n == 0 {
            return Ok(0);
        }
        
        Ok((1u64 << n) - 1)
    }
    
    // 获取棋盘上的所有格子信息
    pub fn board_info(&self) -> Vec<(u32, u64)> {
        (1..=64)
            .map(|square| (square, 1 << (square - 1)))
            .collect()
    }
    
    // 计算达到指定麦粒总数需要多少个格子
    pub fn squares_needed_for_grains(&self, target: u64) -> u32 {
        if target == 0 {
            return 0;
        }
        
        64 - target.leading_zeros()
    }
}

// 便利函数
pub fn square(s: u32) -> Result<u64, GrainError> {
    ChessBoard::new().grains_on_square(s)
}

pub fn total() -> u64 {
    ChessBoard::new().total_grains()
}

// 支持更大棋盘的版本
pub struct LargeChessBoard {
    size: u32,
}

impl LargeChessBoard {
    pub fn new(size: u32) -> Self {
        LargeChessBoard { size }
    }
    
    pub fn grains_on_square(&self, square: u32) -> Result<u128, GrainError> {
        if square >= 1 && square <= self.size {
            Ok(1 << (square - 1))
        } else {
            Err(GrainError::InvalidSquare)
        }
    }
    
    pub fn total_grains(&self) -> u128 {
        (1u128 << self.size) - 1
    }
}

实际应用场景

Grains问题在实际开发中有以下应用:

  1. 算法复杂度分析:理解指数增长的时间复杂度
  2. 密码学:理解密钥空间大小的重要性
  3. 金融计算:复利计算和投资增长模型
  4. 网络拓扑:理解网络连接数的增长
  5. 生物信息学:细胞分裂和种群增长模型
  6. 游戏开发:经验值和等级系统设计
  7. 系统设计:理解数据增长对存储和性能的影响

算法复杂度分析

  1. 时间复杂度

    • square函数:O(1) - 位运算是常数时间操作
    • total函数:O(1) - 按位取反是常数时间操作
  2. 空间复杂度:O(1) - 只使用常数额外空间

与其他实现方式的比较

rust 复制代码
// 使用数学库的实现
pub fn square_pow(s: u32) -> u64 {
    match s {
        1..=64 => 2u64.pow(s - 1),
        _ => panic!("Square must be between 1 and 64"),
    }
}

pub fn total_pow() -> u64 {
    2u64.pow(64) - 1
}

// 使用循环的实现(效率较低)
pub fn square_loop(s: u32) -> u64 {
    match s {
        1..=64 => {
            let mut result = 1;
            for _ in 1..s {
                result *= 2;
            }
            result
        },
        _ => panic!("Square must be between 1 and 64"),
    }
}

// 使用迭代器的实现
pub fn square_iter(s: u32) -> u64 {
    match s {
        1..=64 => (0..s-1).fold(1, |acc, _| acc * 2),
        _ => panic!("Square must be between 1 and 64"),
    }
}

pub fn total_iter() -> u64 {
    (1..=64).map(|s| square(s)).sum()
}

// 使用bigint支持任意大小棋盘的实现
use num_bigint::BigUint;

pub fn square_big(s: u32) -> BigUint {
    BigUint::from(2u32).pow(s - 1)
}

pub fn total_big(board_size: u32) -> BigUint {
    BigUint::from(2u32).pow(board_size) - BigUint::from(1u32)
}

总结

通过 grains 练习,我们学到了:

  1. 位运算:掌握了左移运算符在计算2的幂时的高效应用
  2. 错误处理:学会了使用panic和Result处理错误情况
  3. 数学计算:理解了指数增长的惊人力量
  4. 常量优化:了解了如何使用按位取反等技巧优化常量计算
  5. 边界检查:学会了如何处理输入验证和边界情况
  6. 性能优化:理解了位运算相比其他数学运算的性能优势

这些技能在实际开发中非常有用,特别是在需要高效数学计算、位操作、错误处理和性能优化的场景中。Grains虽然是一个简单的数学问题,但它涉及到位运算、错误处理和算法优化等许多核心概念,是学习Rust高效编程的良好起点。

通过这个练习,我们也看到了Rust在位运算和错误处理方面的强大能力,以及如何用安全且高效的方式实现数学计算。这种结合了安全性和性能的语言特性正是Rust的魅力所在。

相关推荐
q***d1734 小时前
Rust在网络中的协议栈
开发语言·网络·rust
星释4 小时前
Rust 练习册 88:OCR Numbers与光学字符识别
开发语言·后端·rust
前端炒粉6 小时前
35.LRU 缓存
开发语言·javascript·数据结构·算法·缓存·js
拓端研究室7 小时前
专题:2025AI产业全景洞察报告:企业应用、技术突破与市场机遇|附920+份报告PDF、数据、可视化模板汇总下载
大数据·人工智能·pdf
星释7 小时前
Rust 练习册 75:ETL与数据转换
开发语言·rust·etl
断剑zou天涯8 小时前
【算法笔记】窗口内最大值或最小值的更新结构
java·笔记·算法
smj2302_796826528 小时前
解决leetcode第3753题范围内总波动值II
python·算法·leetcode
A尘埃8 小时前
Flink实时数据处理
大数据·flink·实时数据处理
金融小师妹10 小时前
基于NLP语义解析的联储政策信号:强化学习框架下的12月降息概率回升动态建模
大数据·人工智能·深度学习·1024程序员节
骑着猪去兜风.10 小时前
线段树(二)
数据结构·算法