BoggleSolver 普林斯顿 算法第四版
文章目录
一、 引言
作业链接https://coursera.cs.princeton.edu/algs4/assignments/boggle/specification.php
作业答疑https://coursera.cs.princeton.edu/algs4/assignments/boggle/faq.php
1. Boggle
Boggle猜谜游戏。Boggle是由Allan Turoff设计并由孩之宝发行的文字游戏。它包括一块由16个立方体骰子组成的板,每个骰子的6个面上各印一个字母。在游戏开始时,摇动16个骰子并随机分配到一个4乘4的托盘中,只有骰子的顶面可见。
合法的单词满足以下规则:
- 一个有效单词必须由一系列相邻的骰子组成。如果两个骰子是水平、垂直或对角相邻的,则两个骰子是相邻的。
- 一个有效单词最多只能出现一次。
- 有效单词必须至少包含3个字母。
- 有效单词必须在字典中(通常不包含专有名词)
以下是一些合法的情况:
2. 计分
使用此表,根据有效单词的长度对其进行评分
3. Qu特殊情况
在英语中,字母Q后面几乎总是紧跟着字母U。因此,一个模具的侧面打印的是两个字母的序列Qu,而不是Q(在形成单词时,这两个字母的序列必须一起使用)。得分时,Qu算作两个字母;例如,单词队列按5个字母的单词计分,即使它是由4个骰子组成的。
4. 任务要求
本次lab的任务就是编写一个Boggle解算器,使用给定的字典在给定的Boggle板中查找所有有效单词
二、分析
1.推荐的实现步骤
- 熟悉BoogleBoard.java数据结构,这个数据结构可以直接使用,不用更改。
- 使用标准数据结构来表示字典,如SET, TreeSet, HashSet.
- 创建数据结构BoggleSolver,编写一个基于深度优先搜索的方法,以枚举可由以下相邻骰子序列组成的所有字符串。也就是说,枚举Boggle图中的所有简单路径(但不需要显式形成图)。现在,忽略特殊的两个字母序列 Qu 。
- 现在,实现以下关键回溯优化:当当前路径对应的字符串不是字典中任何单词的前缀时,无需进一步扩展路径。为此,需要为支持前缀查询操作的词典创建数据结构:给定前缀,词典中是否有以该前缀开头的单词?
- 处理特殊的两个字母序列 Qu 。
二、代码分析与实现
1.针对dice,创建图(非显示)
创建无向图的邻接矩阵,但不使用官方包里面 graph 数据结构,而是使用 bag 直接创建
代码实现:
private void CreatAdj(BoggleBoard board){
this.board=board;
cols= this.board.cols();
rows= this.board.rows();
adj= new Bag[cols*rows];//建立bag,长度为dice个数
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
int v=i*cols+j;//当前dice的索引
adj[v] = new Bag<Integer>();//为每个dice建立一个bag
//八个方向
if(isExist(i-1,j-1)) adj[v].add((i-1)*cols+(j-1));//left_top
if(isExist(i-1,j)) adj[v].add((i-1)*cols+(j));//top
if(isExist(i-1,j+1)) adj[v].add((i-1)*cols+(j+1));//right_top
if(isExist(i,j+1)) adj[v].add((i)*cols+(j+1));//right
if(isExist(i+1,j+1)) adj[v].add((i+1)*cols+(j+1));//right_bottom
if(isExist(i+1,j)) adj[v].add((i+1)*cols+(j));//bottom
if(isExist(i+1,j-1)) adj[v].add((i+1)*cols+(j-1));//left_bottom
if(isExist(i,j-1)) adj[v].add((i)*cols+(j-1));//left
}
}
// for(int i=0;i< adj.length;i++){
// int r=i/cols;
// int c=i%cols;
// StdOut.printf("(%d,%d): ",r,c);
// for(Integer s:adj[i]){
// StdOut.printf(" (%d,%d)",s/cols,s%cols);
// }
// StdOut.println();
// }
}
2. DFS实现(递归实现)
DFS的实现参照了博主的实现方式:首先,DFS的模板大体不变,可参照书上的DFS建立,但具体实现有所不同。考虑如下问题:
假设我们对点0进行DFS,当搜索到的路径为0-1-6时,按照书上例子,我们会对0、1、6都进行了标记,即在以后0,1,6都不会出现再搜索路径中,这与我们要求的不符(如0-5-1就不会在以后的路径中出现)。实际上我们只需要mark当前的搜索路径即可。
考虑到DFS的非递归实现采用栈stack,因此可建立一个stack保存当前的搜索路径,入栈标记,出栈则取消标记。
如下是代码实现:
private void dfs(Integer v, Node x, String str, Stack<Integer> visitingDices){
if(str.length()>=3&&x!=null){
if(x.val==1){
validwords.add(str);
}
}
for(Integer s:adj[v]){
char c = getLetterOnBoard(s);
//Queue<String> tmp= (Queue<String>) dic.keysWithPrefix(str+c);
if(!marked[s] &&x!=null){
if(x.next[c - 'A'] != null){
visitingDices.push(s);
marked[s]=true;
if(c=='Q'){
dfs(s,x.next['Q'-'A'].next['U'-'A'],str+"QU",visitingDices);
}else{
dfs(s,x.next[c-'A'],str+c,visitingDices);
}
int index = visitingDices.pop();
marked[index]=false;
}
}
}
}
3.前缀搜索
参考书上的算法5.4 基于单词查找树的符号表
实现 get 和 put 的 API。并优化Node的大小为26,即字母表长度。
private static class Node{
private int val;
private final Node[] next = new Node[26];
}
private int get(String key){
Node x= get(root, key, 0);
if(x==null) return 0;
return x.val;
}
private Node get(Node x,String key,int d){
if(x==null) return null;
if(d==key.length()) return x;
int c = key.charAt(d)-'A';
return get(x.next[c], key, d+1);
}
private void put(String key,int val){
root=put(root, key, val, 0);
}
private Node put(Node x, String key,int val,int d){
if(x == null) x = new Node();
if(d == key.length()){
x.val = 1;
return x;
}
int c=key.charAt(d)-'A';
x.next[c] = put(x.next[c],key,val,d+1);
return x;
}
4.详细代码
总结
本次作业难度尚可,也参考了较多博客,DFS实现和前缀搜索的实现较为困难,但好在书上的例子作为参考,因此也能较为顺利的完成。最终代码的得分为95/100