北航软件工程最长单词链
Posted gtwc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了北航软件工程最长单词链相关的知识,希望对你有一定的参考价值。
Part.1
Part.2
PSP2.1 | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 15 | 10 |
PSP2.1 | 估计这个任务需要多少时间 | 15 | 10 |
Development | 计划 | 1120 | 1360 |
· Analysis | 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | 生成设计文档 | 30 | 30 |
· Design Review | 设计文档复审 | 10 | 10 |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 30 |
· Design | 具体设计 | 40 | 60 |
· Coding | 具体编码 | 500 | 480 |
· Code Review | 代码复审 | 100 | 100 |
· Test | 测试(自我测试,修改代码,提交修改 | 300 | 480 |
Reporting | 报告 | 90 | 90 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 计算工作量 | 20 | 20 |
合计 | 1225 | 1465 |
Part.3
Information Hiding的实践:计算核心代码为ChainSolver类,其只有一个对外public函数get_max_chain,内部计算细节都被封装成黑盒,完成了Information Hiding的要求。
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).
Interface Design的实践:Core类作为一个纯C类型的模块调用核心ChainSolver,它作为核心计算类的对外接口体现了松散耦合的思想,比如可以调用交换核心模块。
Loose Coupling的实践:UIUtility是UI控制部分,我们尽量避免了该部分和UI形式上的关联,不使用其他类中的方法进行字符串处理等等。
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.[1] Loose coupling is the opposite of tight coupling.
Part.4
a.算法概述
我们认为这次结对编程的题目可以抽象为有向有环图求最长路径,目前我们使用的算法还是深度遍历穷举所用可能的路径,该图有26个点对应26个英文字母,每个边代表输入的单词,例如输入"hello"可以化为从点‘h‘到‘o‘的边。
首先根据五项需求-w-r-h-t-c, 其中单独的-w是可以约化到-r的,而单独的-r是不能约化到-w,对于-h-t只是对路径首尾进行检测约束,对于-c只是将图的边加权。所以算法应该针对-r设计。深度遍历的过程将从一个点开始,例如‘h‘,如果该点有指向点的边,则递归访问下一点,下一点的递归完成后才会访问‘h‘的其他边的指向。
b.算法数据结构
边和点的struct分别是:
struct Edge {
std::string word;
int code;
int weight;
int next;
};
边Edge的word存储输入的原始字符串;code是该边的识别编码;weight是该边权重,weight默认为1,在-c时为字符串长度;next是字符串末尾字符ascii码-‘a‘的值,方便将点的编码都归为0-25。
struct WordMap {
std::vector<Edge> toLast;
};
点WordMap只包含一个边的vector。
c.类和函数
核心计算包括两个类Core和ChainSolver,Core负责标准输入,所有外部类和函数只调用Core,Core再调用ChainSolver的get_max_chain函数。
ChainSolver类有三个函数公有函数get_max_chain、私有函数CreateMap、私有函数Recursion,get_max_chain调用CreateMap来创建图,再调用Recursion递归DFS创建过的图。
get_max_chain函数
接收以下参数:
char *input[], int num, char *result[], char head, char tail, bool isGetMaxChar, bool enable_loop
以上参数分别是输入的单词数组,输入的单词量,需要输出结果的result,head为-h的头部要求,tail为-t的尾部要求,isGetMaxChar是-c的边权重修改要求,enable_loop是-r的要求。
CreateMap函数
这个部分需要考虑重复输入的单词并将其抛弃,map<string,int>类型的inputWord存储的是已录入的单词。
if (inputWord.find(s) != inputWord.end()) {
return 0;
}
inputWord.insert(std::pair<std::string, int>(s, code));
单词编码部分使用最普通的线性编码0-100,最开始想到过APHash编码但是对于作业里面的小规模的输入太过拖沓。
Recursion函数
使用的是递归法DFS,对传入节点进行路径穷举遍历,进入过的点和边都会被记载在isUsedPoint和isUsedEdge中,递归返回后再将记载记录释放。优化部分会在第六节描述。
Part.5
UML图,同时也展示了内部函数调用情况:
Part.6
我们对DFS穷举法的主要优化是自环剪枝,很明显在求-r最长路径的过程中自环是必定需要走的边,不需要再递归判定了,所以在Recursion进入节点的时候先将该节点所有未走过的自环边加入路径中。
//ensure the edge that wait to push is not used before.
if (isUsedEdge[iter.code])
continue;//'continue' will jump this edge.
//push every self-circle edge.
path.push_back(iter.word);
if (iter.next == point) {
len+=iter.weight;
continue;
}
...
...
...
//pop every self-circle edge.
for (auto iter : map[point].toLast) {
if (iter.next == point) {
isUsedEdge[iter.code] = false;
path.pop_back();
}
else {
break;
}
}
输入为55个完全随机的单词的情况下,VS性能分析结果如下:
从图中可以看到ChainSolver::get_max_chain函数占用CPU资源总计约30%, 占整个后端main调用的四分之三,递归遍历毫无疑问是整个项目里面的性能瓶颈。
Part.8
Core模块的单元测试覆盖率:
对Core模块进行单元测试的过程是:构造测试样例=>传入进Core模块=>检查返回的单词链的长度与内容是否正确。
其中,我们将后两步独立出来,封装进了testutil的两个函数中。
两个函数分别是:
void testRight(char* words[], int words_len, char* res[], int res_len, bool is_max_char, char head, char tail, bool enable_loop);
void testRightMulti(char* words[], int words_len, vector<char**> res, vector<int> res_len, bool is_max_char, char head, char tail, bool enable_loop);
两个函数的区别在于testRightMulti允许符合要求的最长单词链有多条,Core模块只要返回其中一条即为正解。
值得关注的是enable_loop参数。
当enable_loop为假时,会按索引序比较正解与Core返回的解;
当enable_loop为真时,只会进行无序比较。
作为展示的一个简单的测试样例:
TEST_METHOD(simple)
{
char* words[] = {
"hello",
"world",
"ofk",
"kw"
};
char* res[] = {
"hello",
"ofk",
"kw",
"world"
};
testRight(words, 4, res, 4);
}
我们构造测试样例的方式有以下四种:
a. 分析作业要求,找出边界条件,构造边界样例。
b. 根据算法的搜索方式的性质,构造针对性样例。
c. 使用简单算法自动生成大样例,构造压力样例。
d. 针对可能被抛出的异常,构造异常样例。
a. 边界样例
我们首先细读作业文档,从中划出可以得出边界条件的语句,然后针对该语句构造边界样例。这一过程在demand_analyze.md的前半段被体现。摘取其中一段:
- 单词链至少两个单词,不存在重复单词
- 构造一个没有任何两个单词能连上的情况
- 构造有多个重复单词的words
- 构造一个首尾字母相同的单词
b. 针对性样例
在完成算法编写后,针对算法中可能存在的分支、迭代,构造针对性样例试图去覆盖,检验是否如预期那样执行。这一过程在demand_analyze.md的后半段被体现。摘取其中一段:
- -c
- 是唯一的单词数量最长也是字母数量最长
- 是单词数量最长中的一个,但是是所有单词数量最长中的字母数量最长的
- 不是单词数量最长的,但是是字母数量最长的。
- 是字母数量最长中的一个。
c. 压力样例
我们设计了一种简单的算法来自动生成大的测试样例。我们称这种算法为PyramidGenerator。
算法的基本思路是构造一个最多只有26个非根节点的树,且该树具有最长深度的节点有且仅有一个。
如图构造树,每一个节点代表一个字母,每条边表示以起点为首字母、以终点为尾字母的一族单词。由此,每条边上都可以生成无数单词。
而如果我们控制root-t-e-o这条路径上的每条边都只生成一个单词,例如:tme-ego,那么该路径的单词连起来就是唯一最长单词链。
通过使用PyramidGenerator,我们可以生成任意大小的无环测试样例。
d. 异常样例
我们简单地针对Core执行中会抛出的异常构造对应的样例,当该样例被输入后,尝试捕获异常。
Part.9
计算部分一处异常处理,当没有启用-r功能却检查到有环图时将抛出异常w_c_h_t_ChainLoop,以下代码的判定条件是该边的终点已被走过,同时没有开启-r,同时不是自环。
if (isUsedPoint[iter.next] && !isEnableLoop && point != iter.next) {
throw w_c_h_t_ChainLoop;
}
另外也需要考虑对Core的单元测试异常抛出,其实以下异常在程序作为整体运行时不会抛出:
传入的尾部要求是否合理:
if (tail_input != 0 && (tail_input < 'a' || tail_input > 'z')) {
throw para_tail_error;
}
传入的头部要求是否合理:
if (head_input != 0 && (head_input <'a' || head_input > 'z')) {
throw para_head_error;
}
传入的enable_loop是否合法:
try {
isEnableLoop = enable_loop;
}
catch(...){
throw para_loop_error;
}
传入的input数组里面是不是的确有num个值:
for (i = 0; i < num; i++) {
try {
CreateMap(input[i], isGetMaxChar);
}
catch (...) {
throw para_input_error;
}
}
传入的res是否能接受结果:
try {
for (auto iter : maxPath) {
char *new_str = new char[iter.length() + 2];
// std::cout << iter << " ";
for (unsigned int j = 0; j < iter.length(); j++)
new_str[j] = iter[j];
new_str[iter.length()] = '