由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾。

刷完欧拉计划中的63道基础题,能学会Rust编程吗?

“欧拉计划”的网址:

https://projecteuler.net

英文如果不过关,可以到中文翻译的网站:

http://pe-cn.github.io/

这个网站提供了几百道由易到难的数学问题,你可以用任何办法去解决它,当然主要还得靠编程,编程语言不限,论坛里已经有Java、C#、Python、Lisp、Haskell等各种解法,当然如果你直接用google搜索答案就没任何乐趣了。

这次解答的是第54题:

https://projecteuler.net/problem=54

题目描述:

扑克手牌

在扑克游戏中,玩家的手牌由五张牌组成,其等级由低到高分别为:

  • 单牌:牌面最大的一张牌。
  • 对子:两张牌面一样的牌。
  • 两对:两个不同的对子。
  • 三条:三张牌面一样的牌。
  • 顺子:五张牌的牌面是连续的。
  • 同花:五张牌是同一花色。
  • 葫芦:三条带一个对子。
  • 四条:四张牌面一样的牌。
  • 同花顺:五张牌的牌面是连续的且为同一花色。
  • 同花大顺:同一花色的10、J、Q、K、A。

牌面由小到大的顺序是:2、3、4、5、6、7、8、9、10、J、Q、K、A。

如果两名玩家的手牌处于同一等级,那么牌面较大的一方获胜;例如,一对8胜过一对5(参见例1);如果牌面相同,例如双方各有一对Q,那么就比较玩家剩余的牌中最大的牌(参见例4);如果最大的牌相同,则比较次大的牌,依此类推。

S代表黑桃(Spade),H表示红桃(Heart),D表示方块(Diamond),C表示梅花(Club),T表示10(Ten),考虑以下五局游戏中双方的手牌:

手牌 玩家1 玩家2 胜者
1 5H 5C 6S 7S KD 2C 3S 8S 8D TD 玩家2
一对5 一对8
2 5D 8C 9S JS AC 2C 5C 7D 8S QH 玩家1
单牌A 单牌Q
3 2D 9C AS AH AC 3D 6D 7D TD QD 玩家2
三条A 同花方片
4 4D 6S 9H QH QC 3D 6D 7H QD QS 玩家1
一对Q 一对Q
最大单牌9 最大单牌7
5 2H 2D 4C 4D 4S 3C 3D 3S 9S 9D
葫芦 葫芦
(三条4) (三条3)

在poker.txt文本文件中,包含有两名玩家一千局的手牌。每一行包含有10张牌(均用一个空格隔开):前5张牌属于玩家1,后5张牌属于玩家2。你可以假定所有的手牌都是有效的(没有无效的字符或是重复的牌),每个玩家的手牌不一定按顺序排列,且每一局都有确定的赢家。

其中有多少局玩家1获胜?

请

先

不

要

直

接

看

答

案

,

最

好

自

己

先

尝

试

一

下

。

解题过程:

遇到一个复杂的问题,可以尝试将问题分解,变为一个个简单的情况,然后慢慢逼近最终的问题。

第一步: 先读文件,将玩家1和玩家2的牌分开。

第22题里已经学会了读文件,并且将字符串分隔成向量,再利用切片功能将前5个赋给玩家1,后5个赋给玩家2。

let data = std::fs::read_to_string("poker.txt").expect("打开文件出错");
let data2 = data.replace("\r\n", "\n");
let lines = data2.trim().split('\n');
for line in lines {
let hand1 = &line[..14];
let hand2 = &line[15..];
println!("{:?} {:?}", hand1, hand2);
}

第二步: 多文件管理

这个项目涉及到手牌、牌张、花色等概念,适合用面向对象的编程思路。Rust项目对多源文件的功能支持也相当不错,main.rs放主程序,poker.rs放扑克相关的模块。

一手牌Hand由多张牌Card组成,一个Card由牌点(用8位整数表示)和花色Suit构成,花色只有4种,适合用枚举表示。Rust里的枚举看上去与C/C#/Java等语言的枚举很像,但实际上它的功能远远不是一个简单的枚举。

// 文件poker.rs
pub enum Suit {
Spade, // 黑桃
Heart, // 红桃
Diamond, // 方块
Club, // 梅花
} pub struct Card {
value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A
suit: Suit,
} pub struct Hand {
cards: Vec<Card>,
}

main.rs需要加一行语句,告诉主程序要使用poker.rs中定义的模块。

mod poker;

这个时候,程序可以编译,会给出几个警告,提示Hand,Card和Suit这些类型从来没用过。

第三步: 构建一张牌Card

我们的任务要通过一个字符串构建出一个Card对象。比如,"8C"构建出梅花8,"TS"构建也黑桃10,"KC"为梅花K,"9H"为红桃9,"4S"为黑桃4。

这个时候要先学会Rust中的Trait概念,Trait这个东西很像Java/C#里的接口,但又不是。Rust内置不支持构造函数,下面这段代码相当于给Card定义了一个静态方法new(),相当于其它语言里的构造函数。

impl Card {
pub fn new(str_card: &str) -> Card {
let first_char = str_card.chars().next().unwrap();
let card_value = "..23456789TJQKA"
.chars()
.position(|c| c == first_char)
.unwrap() as u8;
let second_char = str_card.chars().nth(1).unwrap();
let card_suit = if second_char == 'S' {
Suit::Spade
} else if second_char == 'H' {
Suit::Heart
} else if second_char == 'D' {
Suit::Diamond
} else {
Suit::Club
};
Card {
value: card_value,
suit: card_suit,
}
}
}

主程序里可以构造一个card(这里用梅花8),尝试打印出来。

let card = poker::Card::new("8C");
println!("{:?}", card);

编译时Rust会给出相当清楚的错误信息,还给出了修改建议

error[E0277]: `poker::Card` doesn't implement `std::fmt::Debug`
--> src\main.rs:14:22
|
14 | println!("{:?}", card);
| ^^^^ `poker::Card` cannot be formatted using `{:?}`
|
= help: the trait `std::fmt::Debug` is not implemented for `poker::Card`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
= note: required by `std::fmt::Debug::fmt`

我们声明了一个新类型Card,但系统并不知道如何把它转换成字符串显示出来。按照提示,我们在Card和Suit前面各加上一行语句,让系统帮我们自动实现一个输出格式。

#[derive(Debug)]
pub enum Suit {
Spade, // 黑桃
Heart, // 红桃
Diamond, // 方块
Club, // 梅花
} #[derive(Debug)]
pub struct Card {
value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A
suit: Suit,
}

此时程序可以顺利编译,运行可以得到如下结果:

Card { value: 8, suit: Club }

输出得虽然有点复杂,但容易理解。如果我们就想输出"8C"这样的字符串,则需要实现Display这个trait里的fmt()函数。注意write!语句后面千万别习惯性地加个分号,否则出现的编译错误让人好困惑!

use std::fmt::{Display, Error, Formatter};
impl Display for Suit {
// 只用一个字母表示: S,H,D,C
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
let name = format!("{:?}", self);
write!(f, "{}", &name[..1])
}
} impl Display for Card {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
let first_char = "..23456789TJQKA".chars().nth(self.value as usize).unwrap();
write!(f, "{}{}", first_char, self.suit)
}
}

现在构建5张牌,输出出来。

let card1 = poker::Card::new("8C");
let card2 = poker::Card::new("TS");
let card3 = poker::Card::new("KC");
let card4 = poker::Card::new("9H");
let card5 = poker::Card::new("4S");
println!("{} {} {} {} {}", card1, card2, card3, card4, card5);

第四步: 构建一手牌Hand

对于Hand,也要实现fmt()函数,还要实现一个构造函数new()。

use itertools::Itertools;
impl Display for Hand {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
let str_cards = self.cards.iter().map(|x| x.to_string()).join(" ");
write!(f, "{}", str_cards)
}
} impl Hand {
pub fn new(str_cards: &str) -> Hand {
let mut v = vec![];
for s in str_cards.split(' ') {
v.push(Card::new(s));
}
Hand { cards: v }
}
}

主程序这样写:

let hand = poker::Hand::new("8C TS KC 9H 4S");
println!("{}", hand);

第五步: 比较两个对象的大小

现在我们想比较两手牌的大小,主程序写成这样。

let hand1 = poker::Hand::new("8C TS KC 9H 4S");
let hand2 = poker::Hand::new("7D 2S 5D 3S AC");
if hand1 > hand2 {
println!("player1 wins" );
}

想让两个对象能够相互比较大小,需要实现四个trait(Ord、PartialOrd、Eq和PartialEq)中的几个函数。

use std::cmp::{Ord, Ordering};
impl Ord for Hand {
fn cmp(&self, other: &Self) -> Ordering {
self.to_string().cmp(&other.to_string())
}
} impl PartialOrd for Hand {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
} impl Eq for Hand {} impl PartialEq for Hand {
fn eq(&self, other: &Self) -> bool {
self.to_string().eq(&other.to_string())
}
}

现在,这里的比较逻辑还没有实现,暂时用字符串的比较代替。有几点要留意:

1)Ord里的cmp()函数,PartialOrd里的partial_cmp()函数,一个是表示全序,一个表示偏序。

2)cmp()和partial_cmp()两个函数的返回值有点区别,后面的多Option<>

3)Eq里的内容是空的,但必须要写

4)PartialEq里的函数名是eq()

5)实现了这些trait后,程序会自动理解“<”、“>”、“==”这些比较运算符。

第六步: 比较两手牌的大小

这时需要细心了,判断同花、顺子、四条、三条、对子等情况,为了后面的比较,我声明了一个枚举enum,用来区分各种牌型,从这里可以领略Rust里枚举的强大。

pub enum HandType {
HighCard(u8, u8, u8, u8, u8), // 单牌
//对子
OnePair {
value_pair: u8,
max_remain: u8, // 除了对子之外,剩下最大的牌点
},
//两对
TwoPairs {
high_pair: u8, // 最大的一对
low_pair: u8, // 最小的一对
max_remain: u8, // 除了两对之外,剩下最大的牌点
},
KindThree(u8), // 三条
Straight(u8), // 顺子
Flush(u8), // 同花
//葫芦,即三条带一个对子
FullHouse {
value_kind_three: u8,
value_pair: u8,
},
KindFour(u8), // 四条
StraightFlush(u8), // 同花顺
RoyalFlush, // 同花大顺
}

再声明两个函数ranking1()和ranking2(),两次比较后能够区分大小。

impl HandType {
pub fn ranking1(&self) -> u8 {
match self {
HighCard(_, _, _, _, _) => 0,
OnePair { .. } => 1,
TwoPairs { .. } => 2,
KindThree(_) => 3,
Straight(_) => 4,
Flush(_) => 5,
FullHouse { .. } => 6,
KindFour(_) => 7,
StraightFlush(_) => 8,
RoyalFlush => 9,
}
} pub fn ranking2(&self) -> u64 {
// ...
}

这里有一个".."的语法点,忽略结构体里的内容。

FullHouse { .. } => 6,

相当于:

FullHouse {
value_kind_three: _,
value_pair: _,
} => 6,

Ord里的cmp()和PartialEq里的eq()的逻辑也要相应修改一下。

impl Ord for HandType {
fn cmp(&self, other: &Self) -> Ordering {
self.ranking1()
.cmp(&other.ranking1())
.then(self.ranking2().cmp(&other.ranking2()))
}
}
impl PartialEq for HandType {
fn eq(&self, other: &Self) -> bool {
self.ranking1() == other.ranking1() && self.ranking2() == other.ranking2()
}
}

剩下就是依次判断各种情况,需要足够的耐心和测试。

use itertools::Itertools;
impl Hand {
//是否五张牌同一个花色。
fn is_flush(&self) -> bool {
self.cards.iter().map(|card| card.suit).all_equal()
} //判断五张牌是否连号,先将牌面数值从小到大排序,两两之差为1就是顺子。
fn is_straight(&self) -> bool {
let mut v: Vec<u8> = self.cards.iter().map(|x| x.value).collect();
v.sort();
(0..4).all(|i| v[i + 1] - v[i] == 1) //两两之差都为1
}

第七步: 将相关类整理到一个文件夹下

源文件较多时,可以放在一个文件夹下,模块里还可以有子模块。比如这样组织文件:

src/
+---main.rs
+---poker/
+---card.rs
+---hand.rs
+---hand_type.rs
+---mod.rs
+---suit.rs

poker文件夹可以自动识别为一个mod,需要mod.rs文件的配合,这里声明用到的子模块,编译器可以自动找到相应的源文件。

pub mod card;
pub mod hand;
pub mod hand_type;
pub mod suit;

hand.rs文件里使用其它模块的内容时,需要用use语句。

use super::card::*;
use super::hand_type::*;

--- END ---

我把解题的过程记录了下来,写成了一本《用欧拉计划学 Rust 编程》PDF电子书,请随意下载。

链接:https://pan.baidu.com/s/1NRfTwAcUFH-QS8jMwo6pqw

提取码:qfha

该PDF文件将来会不定期更新,可以在公众号后台回复“rust”,得到最新的下载链接。

历史文章:

学会10多种语言是种什么样的体验?

刷完欧拉计划中的63道基础题,能学会Rust编程吗?

通过欧拉计划学Rust编程(第54题)的更多相关文章

  1. 通过欧拉计划学Rust编程(第500题)

    由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾. "欧拉计划"的网址: https://projecteuler.net 英文如果不过关 ...

  2. 用欧拉计划学Rust编程(第26题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  3. 通过欧拉计划学习Rust编程(第22~25题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  4. 用欧拉计划学Rust语言(第17~21题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  5. 用欧拉计划学习Rust编程(第13~16题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  6. 用欧拉计划学Rust语言(第7~12题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  7. 通过欧拉计划学Rust(第1~6题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,看来想准确理解MOVE的机制,还需要对Rust有深刻的理解,所以开始了Rust的快速入门学习. 看了一下网上有关Rust的介绍,都 ...

  8. 刷完欧拉计划中难度系数为5%的所有63道题,我学会了Rust中的哪些知识点?

    我为什么学Rust? 2019年6月18日,Facebook发布了数字货币Libra的技术白皮书,我也第一时间体验了一下它的智能合约编程语言MOVE,发现这个MOVE是用Rust编写的,看来想准确理解 ...

  9. 【欧拉计划4】Largest palindrome product

    欢迎访问我的新博客:http://www.milkcu.com/blog/ 原文地址:http://www.milkcu.com/blog/archives/1371281760.html 原创:[欧 ...

随机推荐

  1. 「Luogu P2015」二叉苹果树 解题报告

    题面 一个二叉树,边数为n\((2<n\le 100)\),每条边有一个权值,求剪枝后剩下p\((1<p<n)\)条边,使p条边的权值和最大 还看不懂?-- 2 5 input:5 ...

  2. 1072 开学寄语 (20分)C语言

    下图是上海某校的新学期开学寄语:天将降大任于斯人也,必先删其微博,卸其 QQ,封其电脑,夺其手机,收其 ipad,断其 wifi,使其百无聊赖,然后,净面.理发.整衣,然后思过.读书.锻炼.明智.开悟 ...

  3. oracle-按年、月、周、日、时、分 分组查询统计数据,无数据补零(connect by)

    目的:统计一段时间内每年.每月.每周.每日.每时.每分数据,无数据时自动补零 思路:1. 生成给定时间段对应日期 2. 将原表中该时间段内的不为0的数据量统计出来 3. 用left join连接起来, ...

  4. 升级添加到现有iOS Xcode项目的Flutter

    如果你在2019年8月之前将Flutter添加到现有iOS项目,本文值得你一看. 在2019年7月30日,合并合并请求flutter / flutter#36793之前Flutter 1.8.4-pr ...

  5. C语言之指针用法总结

    C语言指针概述:1.计算机系统中,无论是存入或是取出数据都需要与内存单元打交道,物理器件通过地址编码寻找内存单元.2.地址编码是一种数据,C语言的指针类型正是为了表示这种计算机所特有的地址数据.3.存 ...

  6. (2)MongoDB副本集自动故障转移原理

    前文我们搭建MongoDB三成员副本集,了解集群基本特性,今天我们围绕下图聊一聊背后的细节. 默认搭建的replica set均在主节点读写,辅助节点冗余部署,形成高可用和备份, 具备自动故障转移的能 ...

  7. kubernetes concepts (一)

    Concepts The Concepts section helps you learn about the parts of the Kubernetes system and the abstr ...

  8. 关于爬虫的日常复习(7)—— DOM操作及selenium库

  9. AI——第四次工业革命

    历史上发生了三次工业革命:第一次是以蒸汽机的发明为代表,改变了长久以来的棉纺织业,人类进入"蒸汽时代":第二次是以汽车的发明和电学的发展为代表,人类的活动范围和时长都极大地扩展,人 ...

  10. 【STACK】Several待填的坑

    待学的习: https://www.cnblogs.com/xiao-ju-ruo-xjr/p/9149792.html 待写的题: loj#3184:「CEOI2018」斐波那契表示法 luoguP ...