实验环境
Windows 11 专业版22H2 Feature Experience Pack 1000.22642.1000.0
Microsoft Visual Studio Code 1.78.1 with MingW-W64-builds 10.0.0 & GCC 12.1.0
Qt Creator 9.0.2 Based on Qt 6.4.2 (MSVC 2019, x86_64)
实验目的
练习递归编程方法.
Problem 1 Game 24
问题分析与算法设计
封装递归函数
Set<string> countTwentyFours(Vector<Card> & cards)
接受牌组cards
作为参数,其遍历牌组中所有手牌,依次抽取一张作为起始牌调用主递归函数,将返回值加入到结果中,最后返回结果.
主递归函数
Set<string> countTwentyFoursRec(const Card result,Vector<Card> remain_cards)
接受两个参数,result
为当前的运算结果,remain_cards
为剩余的手牌,实现过程大致如下:
Set<string> countTwentyFoursRec(const Card result,Vector<Card> remain_cards)
{
Set<string> solutionsStrings={};
if(剩两张牌)
{
solutionsStrings+=对剩余两张手牌进行运算合并后调用递归;
}
if(剩一张牌)
{
solutionsStrings+=最后一张手牌可以与result运算后达到24点的方法;
return solutionsStrings;
}
for(遍历remain_cards)
{
solutionsStrings+=依次抽走一张牌与result进行四则运算后调用递归;
}
return solutionsStrings;
}
测试数据与结果
实验小结
已知的问题有:
- 目前算法无法排除加法/乘法因为交换律可能出现的重复情况(比如
( ( ( 5C + 10S ) - 9C ) * 4H )
和( ( ( 10S + 5C ) - 9C ) * 4H )
将会被视为两种不同的结果); - 目前结果的输出形式只针对4张手牌进行了考虑,包含了两种可能的情况:3-1式(比如
( ( ( 5C + 10S ) - 9C ) * 4H )
)或者2-2式(比如( ( 9C - 5C ) * ( 10S - 4H ) )
),在四张以上手牌的游戏情况下有可能出现漏解的可能性;
Problem 2 The Game of Boggle
问题分析与算法设计
Task 1—Dice reading, board drawing, dice shaking.
这一部分相关的类/函数声明与实现分别在dice.h
与dice.cpp
中.
- 绘制面板使用
gboggle.cpp
中的drawBoard()
函数来完成; - 单独设计
Dice
类来完成与骰子相关的基础操作: - 私有成员:
string letters
存储骰子六个面的字母(大写); - 构造函数:
Dice(string str)
接受一个长度为6的字符串str
作为参数(如果长度小于6报错无效输入,大于6自动截取前6位),str
的每一位对应letters
中的每一面; char diceShaking()
方法,调用radom.h
库辅助,返回letters
中的随机一个字母;string getLetters()
方法,返回私有成员letters
;- 基于
Dice
类设计函数来完成整体的与骰子相关操作: Vector<Vector<Dice>> read_all_dices(ifstream& file,int n)
函数从文件中读取信息,返回一个n阶的二维Dice
容器;Vector<Vector<char>> shake_all_dices(Vector<Vector<Dice>> dices)
函数随机掷出所有骰子,并且将结果绘制在面板上,同时返回一个小写的字母容器方阵来记录结果;
Task 2—User’s turn (except for finding words on the board)
这一部分相关的类/函数声明与实现分别在user.h
与user.cpp
中.
- 启动函数:
Set<string> user_turn(Vector<Vector<char>> cubes)
- 接受一个骰子的掷取结果方阵作为参数,用于构造类型
- 输出启动提示词;
- 构建一个
User
类(关于类的具体说明在后); - 调用
User::input()
方法,使用户持续输入 - 返回用户最终的输入结果,供电脑的回合调用来避免重复;
- 辅助函数:
string str_tolower(string str)
,返回str的全小写形式,来消除大小写的区别对重复性的影响 User
类的具体说明- 私有成员:
Set<string> inputed
来记录用户已经输入的单词;Vector<Vector<char>> cubes
来记录当前用户需要处理的骰子掷取结果;
- 构造函数
User(Vector<Vector<char>> cubes)
接受一个骰子的掷取结果方阵作为参数,并将其传入对应的私有成员; void input()
方法:向inputed
成员输入单词,该方法在本任务的部分检查以下输入合法性:str
的长度是否小于4,如果是则要求重新输入str
的转换小写后结果是否已经在inputed
中存在,如果是则要求重新输入;
Set<string> get_inputed()
方法:返回私有成员inputed
Task 3—Find a given word on the board.
这一部分相关的类/函数声明与实现分别在user.h
与user.cpp
中.
- 在本阶段
User::input()
方法检查以下输入的合法性: - 配合
Lexicon
类字典检查输入单词是否为有效存在的英语单词; - 通过递归函数
findGivenWord()
函数(具体说明在后)检查输入单词是否能在结果方阵上被找到; - 辅助函数
bool ifValidIndex(int index,int upper)
返回一个数组索引是否合法,用来避免递归时可能出现的数组越界问题;index
代表目标索引,upper
代表索引可以接受的最大上限(开区间); - 辅助函数
void doHighlight(Vector<Vector<int>> findRes)
配合gboggle.h
中的highlightCube()
函数对搜索到的结果进行单词高亮操作,接受一个递归的搜索结果findRes
作为参数传入; - 封装递归函数
Vector<Vector<int>> findGivenWord(string word,Vector<Vector<char>> cubes)
接受一个目标单词word
和结果方阵cubes
作为参数,返回一个二维的整型数容器,用来记录搜索到的轨迹,实现思路如下: - 遍历搜索结果方阵,查找单词的首字母是否存在于首字母中;
- 当出现首字母后,调用主递归函数
findGivenWordRec()
进一步搜索结果,如果找到结果则直接返回,否则继续搜索; - 如果遍历结束仍然未搜索到结果,返回空容器;
- 主递归函数
Vector<Vector<int>> findGivenWordRec(string word,Vector<Vector<char>> cubes,Vector<Vector<int>> way,int word_len)
接受四个参数,前两个与findGivenWord()
函数相同,way
参数传入已经记录的轨迹,word_len
参数传入目标单词的长度,实现思路如下: - 当
way.size()==word_len
,此时所有字母已经被全部找到,返回way
作为最终结果; - 如果不符合递归结束条件,取
way[way.size()-1]
的值作为current_pos
,即当前位置; - 配合
ifValidIndex
函数搜索当前位置周围一圈符合规则的字母,并且检查是否为当前word
的首字母; - 如果找到了符合要求的字母,对
word
截去首字母作为new_word
,对way
加入字母坐标作为new_way
,将该字母在cube
中的值赋0来避免重复使用,用这些新的参数继续递归,直到找到结果; - 如果最终依然没有找到结果,返回空容器;
- 最终
User::input()
方法的实现方法大致如下:
void User::input()
{
while(true)
{
输入word并将其自动转换为小写形式
/*检查合法性阶段*/
if(word为空)
break;
if(word长度<4)
continue;
if(word已经被输入)
continue;
if(word不在字典中)
continue;
if(递归结果为空)
continue;
/*录入阶段*/
inputed.add(word); // 向inputed成员录入单词
doHighlight(findRes); // 高亮单词
recordWordForPlayer(word,HUMAN);// 为玩家计分
}
}
Task 4—Find all the words on the board (computer’s turn)
这一部分相关的类/函数声明与实现分别在computer.h
与computer.cpp
中.
- 启动函数
void computer_turn(Vector<Vector<char>> cubes,Set<string> user_found)
- 接受两个参数传入:结果方阵
cubes
和用户找到的单词user_found
; - 使用传入的参数构造一个
Computer
类(类型说明在后); - 调用
Computer::findAllWords()
方法递归找到所有单词; - 为电脑记录单词与分数;
Computer
类:- 私有成员:
Set<string> user_found
用户已经找到的单词;Set<string> computer_found
电脑找到的单词;Vector<Vector<char>> cubes;
需要处理的结果方阵;
- 构造函数:
Computer(Vector<Vector<char>> cubes,Set<string> user_found)
接受两个参数并且传入到对应的私有成员; - 方法
Set<string> get_user_found()
&Set<string> findAllWords()
返回对应的私有成员; - 方法
Set<string> findAllWords()
封装的递归函数,遍历cube
中的每一个元素,将其作为首字母调用主递归函数findAllWordsRec()
并将返回值加入到结果中,最后返回完整的结果; - 主递归函数:
findAllWordsRec()
: - 完整形式为
Set<string> findAllWordsRec(Vector<Vector<char>> cubes, Set<string> user_found, string current_word, Vector<int> current_pos, Set<string> found);
参数说明:cubes
:当前处理的结果方阵;user_found
:用户已经找到的单词;current_word
:目前搜索路径的对应单词;current_pos
:目前的搜索位置;found
:电脑已经找到的单词;
- 实现过程:
- 将参数
found
拷贝到当前结果result
中; - 如果字典中已经不存在
current_word
开头的单词,则递归结束,返回result
; - 如果
current_word
满足:长度大于4且用户没找到且在字典中,则将其添加到result
中; - 配合
isValidIndex()
函数搜索current_pos
周围一圈合法的cube
元素,分别将新字母元素添加到new_word
中,更新位置next_pos
,最后使用新参数调用递归,将结果添加到result
中; - 遍历搜索完成后,返回
result
;
- 将参数
Boggle.cpp
最终实现的游戏流程函数调用如下:
void playBoggle() {
GWindow gw(BOGGLE_WINDOW_WIDTH, BOGGLE_WINDOW_HEIGHT);
gw.setLocation(50,50);
initGBoggle(gw);
while (true)
{
/*Task 1 — Dice reading, board drawing, dice shaking.*/
// 画板
drawBoard(4,4);
// 读取骰子
ifstream dice_data("res/cubes16.txt");
Vector<Vector<Dice>> dices=read_all_dices(dice_data,4);
dice_data.close();
// 掷骰子
Vector<Vector<char>> cubes=shake_all_dices(dices);
/*Task 2&3 — User's turn*/
auto user_found=user_turn(cubes);
/*Task 4 — Find all the words on the board (computer's turn)*/
computer_turn(cubes,user_found);
if (!getYesOrNo("Would you like to play again? ")) break;
}
exitGraphics();
}
测试数据与结果
实验小结
In Task1
read_all_dices()
函数逻辑上参数n
对应的是列的数量(每行的数据量达到列数换行),但是考虑到题目给出的数据是4阶或5阶方阵,所以简记为n;- 当输入输出流作为参数传入函数时不能使用值传递,必须使用引用传递的方式,即
ifstream&
;
In Task2
- 本来while循环是写在
user_turn()
函数中,而User::input()
方法通过传入一个字符串参数进行单次输入,但是这种方式存在无法顺利跳出循环的可能性(比如先输入一个不符合的单词→程序要求重新输入→输入空字符串结束循环→程序认为输入小于4要求重新输入)于是最后决定将整个输入过程全部封装在User::input()
方法中,且此时该方法不需要任何参数传入;
In Task3
- 考虑到Words should be considered case insensitively: PEACE is the same as peace.的要求,所有的输入最后全部转换为小写形式,这和
cubes
均为小写形式保持了一致;
In Task4
- 感觉实际的运行速度似乎比Demo程序慢上了一点?
- 本来在
findAllWordsRec()
函数中用了和findGivenWordRec()
函数中一样的way
作为参数传入,但是在计算机搜索的过程中似乎没有记录完整路径的必要,只需要保留最后一个当前位置即可,于是最后修改参数为current_pos
;