这是一篇课程博客。
Q | A |
---|---|
这个作业属于哪个课程 | 2020春北航计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 个人项目作业 |
我在这个课程的目标是 | 系统地学习软件工程理论知识与实践方案 |
这个作业在哪个具体方面帮助我实现目标 | 体验PSP流程;尝试编写高质量软件 |
作业代码在哪里 | https://github.com/Mistariano/intersect |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20min | 20min |
· Estimate | 估计这个任务需要多少时间 | 5min | 5min |
Development | 开发 | 3h | 2.5h |
· Analysis | 需求分析 (包括学习新技术) | 10min | 40min |
· Design Spec | 生成设计文档 | 10min | 10min |
· Design Review | 设计复审 (和同事审核设计文档) | 0min | 0min |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10min | 0min |
· Design | 具体设计 | 30min | 50min |
· Coding | 具体编码 | 3h | 2h |
· Code Review | 代码复审 | 0min | 0min |
· Test | 测试(自我测试,修改代码,提交修改) | 40min | 10min |
Reporting | 报告 | 40min | 20min |
· Test Report | 测试报告 | 20min | 0min |
· Size Measurement | 计算工作量 | 6h | 4h |
· Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10min | 10min |
合计 | 15h15min | 11h15min |
回顾
这次作业和eccv、美赛撞个满怀,时间过于紧张(作业3月3号发布,但直到3月6日早上5点前还在改准备投出的论文、完善说明和实验。3月6日8点又放出了美赛题目,几乎没有休息就又全身心投入比赛,一直到3月10日临近中午才全部完成。加上下午的课从2点上到4点,实际留给最后编码的时间只有3个小时……加上这周中间几个小时段的穿插,这次作业总共投入了不到10个小时的时间,和之前的投入实在没法相提并论。)
在任务规划时已经把各方面的时限压到最低(见上面的PSP,对于一次大作业来说规划的工作强度接近个人极限了),因此完成质量实在难以保持,做的不太好,很多地方没有完善。
虽然狼狈如此,这次作业依旧有很多可以总结的东西,我觉得这就是软工方法论中最有意思的地方之一:总是有可以总结的经验;总是有机会学习所有经验。
理解题目
要求平面内交点数量,存在多线共点
难点主要在于处理浮点数精度和处理极大的输入规模(50w条线,500w个点,复杂度相对于交点应该不能超过MlogM)
考虑引入分数类处理,使用搜索树(STL set,红黑树)借助自己实现的compare维护相近点查找
具体细节
首先根据输入的点将直线表示为一般形式(y=ax+b)(需要注意直线与y平行的特殊情况,后面求交点一步中单独处理),然后对于所有直线,计算其与其他直线的交点位置,这一步需要N^2/2的开销,N是线的数量
中间所有计算结果使用分数类维护,记录分子和分母。这里有两个trick点,
- 对每个分数用fp32记录一个粗略的估计值
rough
,这样对于两个分数的比大小我们可以由粗到细的实现:先比较rough的结果,如果差值在浮点精度epsilon以外就可以直接得到比较结果,只有精度无法判断两者是否相等时才比较分数结果 - 对于分数的比较,比较前显然需要一次约分,这里约分采用lazy策略处理,对每个分数标记
simplfied
,只有在需要约分时才对未约分过的分数约分。这样在搜索树中找到插入位置前,分数类的数值比较不会带来过多的额外开销。
代码设计
考虑到功能需求以数值计算为主,没有特别强烈的扩展性需求及代码复用动机,且对性能要求较高,选择采用面向过程的范式进行设计(只有在涉及容器调用及编写Compare函数类时才涉及到一小部分面向对象和泛型特性)。
对于面向过程程序,函数的封装粒度是尤其需要控制,一个适当的封装粒度不仅不会增加工程复杂性,而且会很大地简化测试过程。这里我的设计采用三层:
- 外层逻辑流控制,输入/输出。
- 分数处理过程,包括分数的预处理、分数运算、分数比较等,见代码fractions.h
- 查找树的封装。之前由于考虑过如果时间允许,使用自己定制的查找树替换stl的红黑树,因此做了一层adaptor,可惜最后时间不够没有进一步实现自己的查找树。这部分见代码trees.h
其中trees.h中,关于比较的一段逻辑比较重要,因此摘录部分代码
// 这个类就是两个分数的具体比较过程。首先根据fp32的低精度值比较,若数值接近再细致比较。
int _cmp_fraction(fraction_t &a, fraction_t &b) {
if (a.rough - b.rough > EPS) {
// a>b
return 1;
}
if (b.rough - a.rough > EPS) {
// a<b
return -1;
}
if (!a.simplified) {
int64_t gcd1 = gcd(a.top_abs, a.bottom_abs);
a.top_abs %= gcd1;
a.bottom_abs %= gcd1;
a.rough = (float) a.sign * (float) a.top_abs / (float) a.bottom_abs;
a.simplified = true;
}
if (!b.simplified) {
int64_t gcd1 = gcd(b.top_abs, b.bottom_abs);
b.top_abs %= gcd1;
b.bottom_abs %= gcd1;
b.rough = (float) b.sign * (float) b.top_abs / (float) b.bottom_abs;
b.simplified = true;
}
int64_t a_top = a.top_abs * a.sign;
int64_t b_top = b.top_abs * b.sign;
if (a_top == b_top && a.bottom_abs == b.bottom_abs) {
return 0;
}
// TODO
// 很遗憾这里最后还差一个分支没有实现完。程序运行到这里的时候,已经可以确定两个分数数值接近但不相等了,因此直接对分数类做差判断分子符号即可。
// 即:判断a_top*b_bottom - a_bottom - b*top的符号
return 1;
}
// 由于每个点有两个分数数值(x和y),因此将x作为第一索引、y作为第二索引,满足全序
int _cmp_before_ins(int f, int i) {
// insert node i to sub-tree whose root is t
auto &fnode = tree_pool[f];
auto &cur = tree_pool[i];
// index1: compare x fraction
int res_x = _cmp_fraction(fnode.x, cur.x);
if (res_x == 0) {
int res_y = _cmp_fraction(fnode.y, cur.y);
return res_y;
}
return res_x;
}
// 比较函数类,通过重载调用运算符使这个类的对象可以像函数一样被调用。
// 这里由于使用了一块内存池tree_pool管理所有结点,因此通过定义索引排序规则实现间接排序
struct Compare {
int operator()(int a, int b) const {
return _cmp_before_ins(a, b) >= 0 ? 0 : 1;
}
};
// 使用Compare作为排序规则的交点集。最后这个集合的size就是需要的答案
std::set<int, Compare> set;
// 结点插入的adaptor。如果之后实现了其他二叉树,修改这里即可
void insert_node(int i) {
auto &cur = tree_pool[i];
int before = set.size();
set.insert(i);
if (set.size() == before) {
free_node();
}
}
CMake与VS2019
由于VS2019引入了对CMake的支持,这次作业在规划时使用个人更熟悉的CMake进行编译管理,因此这份代码可以拜托对VS的限制,在任何平台都可以几乎零改动完成编译。
但很遗憾规划任务时没有注意到VS的CMake支持并不完善,像单元测试生成、代码质量分析等工具的使用方式和传统VS sln解决方案并不相同,而个人对这些特性本身就不熟悉。因此在追求快速开发、于Jetbrains Clion上完成代码主体后,尝试在VS上实现课程组要求的分析任务时严重受挫,最终没有完成。
即使如此,我也尽可能注意了代码质量的控制,包括对代码风格的约束(虽然没有来得及修改格式配置,很多风格与课程推荐有出入)、必要注释的添加和一些基础的黑箱测试(平行线、多点共线、与坐标轴平行的线、交点十分接近的线、构造大质数样例测数值溢出风险等)。很可惜这些测试没有办法体现在代码里。
总结
虽然是很狼狈的一次作业,但回顾一下,我对自己在相当短的时间内走完了分析、设计、测试和总结的PSP流程总体还是满意的。由于这次不可抗力太多而导致的投入严重不足,我对作业分数有相当的心理准备,但至少这是一次高压下的自我操练,我依然很珍惜并感谢这次开发经历。
希望这周的结对编程好好表现 ,不要坑害队友啦。