结对项目(段翔晖,王尉人)
| 姓名 | 学号 | GitHub |
|---|---|---|
| 段翔晖 | 3124004167 | https://github.com/zayuchips/3124004167-pair-project |
| 王尉人 | 3124004180 | https://github.com/akcow |
| 这个作业属于哪个课程 | 计科24级78班 软件工程 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 两人合作完成一个「自动生成小学四则运算题目并判卷」的命令行程序,练习模块化设计、单元测试、性能分析与 PSP 流程 |
程序语言:Python 3(只用标准库)| 代码目录:仓库根目录下 3124004167/
一、PSP 表格(开工前的预估)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 20 |
| Development | 开发 | 400 |
| · Analysis | · 需求分析(包括学习新技术) | 60 |
| · Design Spec | · 生成设计文档 | 30 |
| · Design Review | · 设计复审(和同事审核设计文档) | 20 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 15 |
| · Design | · 具体设计 | 45 |
| · Coding | · 具体编码 | 150 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 50 |
| Reporting | 报告 | 90 |
| · Test Report | · 测试报告 | 30 |
| · Size Measurement | · 计算工作量 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 45 |
| 合计 | 520 |
开工前我们对任务做了拆解:先把「题目要求」逐条抄下来变成一张清单(-n、-r、不许出负数、除法结果必须是真分数、运算符不超过 3 个、题目不能重复、写两个文件、支持一万道、判卷输出 Grade.txt),再按「表达式生成 / 判重 / 文件读写 / 判卷 / 性能」五块分工。事后看,这个拆解对预估帮助很大,但测试和具体编码两项仍然超了。
二、效能分析
2.1 怎么采样的
在命令行下用 Python 自带的 cProfile 跑一万道题(就是需求 9 要求的那一万道),只看我们自己写的模块:
python3 profile_report.py
这个脚本会做三件事:真的生成一万道题、打印「自身耗时 tottime」和「累计耗时 cumtime」两张排名表、再把排名画成一张图(profile_chart.png)。
2.2 采样结果
在生成一万道题、用时 0.40 秒 的那次采样里,自身耗时(tottime)排名前 8 是:
| 函数 | 自身耗时(秒) | 占比 | 调用次数 |
|---|---|---|---|
| build_tree (expression.py) | 0.036 | 9.0% | 10028 |
| new (fractions.py) | 0.031 | 7.7% | 66746 |
| _randbelow_with_getrandbits (random.py) | 0.030 | 7.5% | 119479 |
| builtins.pow | 0.027 | 6.9% | 60148 |
| format_fraction (fraction_utils.py) | 0.024 | 6.1% | 40046 |
| randrange (random.py) | 0.023 | 5.7% | 94008 |
| hash (fractions.py) | 0.021 | 5.2% | 60148 |
| random_operand (expression.py) | 0.019 | 4.7% | 38950 |
累计耗时(cumtime)排名前几:
| 函数 | 累计耗时(秒) | 说明 |
|---|---|---|
| make_problems (generator.py) | 0.394 | 出题主流程,占了几乎全部时间 |
| random_expression (expression.py) | 0.231 | 摇一道题的总开销(含重摇) |
| build_tree (expression.py) | 0.217 | 递归造表达式树 |
| random_operand (expression.py) | 0.074 | 生成操作数 |
| apply_op (expression.py) | 0.043 | 顺手算当前节点的值 |
消耗最大的函数:build_tree。 自身耗时第一,累计耗时也仅次于它的上层函数;它是递归造表达式的核心,所有约束检查(减法不能出负数、除法的商必须是真分数)都在里面完成,一次造不成还要重摇。相对「冤枉」的开销是 random 与 Fraction 的内部调用——一万道题要摇出 10 万个随机数、构造 6 万多个 Fraction,这部分是标准库的固有成本。
性能分析图(左:自身耗时;右:累计耗时):
!性能分析图:左为自身耗时 tottime,右为累计耗时 cumtime(cProfile 采样,一万道题
性能分析图:左为自身耗时 tottime,右为累计耗时 cumtime(cProfile 采样,一万道题
2.3 为了提速做了什么
- 判重从「逐个比较」改成「规范化指纹 + 集合」。 一开始判重是把新题目和已经出过的题目一条条比:这是 O(n²)。实测 3000 道题做一次朴素判重需要 152 毫秒,换成集合后只要 5.2 毫秒,快了约 29 倍;按平方增长推算,一万道题时朴素做法要 1.7 秒左右,已经超过现在整个程序 0.4 秒的总耗时了。
- 数值一律用
fractions.Fraction,不用浮点数。 浮点数在小数上是近似的:0.1 + 0.2得到0.30000000000000004,作业原文的例子1/6 + 1/8用浮点算出来是0.29166666666666663,而用Fraction得到的是精确的7/24,输出到答案文件时正好是题目要求的写法。这一条不是「更快」,而是「更对」——分值最高的 20 分是功能分,答案错一切白搭。 - 约束前置到生成阶段。 不是先生成一大堆再筛掉,而是在
build_tree造每个节点时就判断:减法若e1 < e2就把左右子树换过来,除法若不满足0 < 被除数 < 除数就直接重摇这个节点。这样返工量小,一万道题很快就能凑齐。 - 顺手做了一层「别太水」的筛选。 第一版会出现
0 × 1/3 × 6 × 7 =、2 × 1 =这种题,虽然合法但没营养,于是当-r >= 4时,加法不要 0、乘法不要 0 和 1、减法不要 0 和被减数相等的情况。范围很小(比如-r 2)时不做这层筛选,否则根本出不了题。
三、设计实现过程
3.1 模块划分
代码按「一个模块一件事」拆成 5 个文件 + 1 个测试文件:
| 文件 | 职责 | 对外主要接口 |
|---|---|---|
main.py |
命令行入口:解析 -n / -r / -e / -a,决定出题还是判卷,检查参数合法性 |
main(argv) |
expression.py |
表达式核心:随机生成、求值、判重指纹、与文字互转 | random_expression / build_tree / expression_key / to_text / parse_tree / evaluate_text |
generator.py |
出题:去重、限次、写 Exercises.txt 与 Answers.txt |
make_problems / write_problems |
checker.py |
判卷:读题目与答案文件、逐题比对、写 Grade.txt |
check |
fraction_utils.py |
分数的文字格式(3/5、2'3/8)与反向解析 |
format_fraction / parse_fraction |
tests/test_program.py |
35 个单元测试 | — |
3.2 表达式的表示:元组树
这是整个程序的地基。一个算式在内存里就是一棵树:
叶子节点:(值,) 例如 (Fraction(1, 2),) 表示 1/2
内部节点:(运算符, 左子树, 右子树) 例如 ("÷", (Fraction(3,4),), (...)) 表示 3/4 ÷ ...
为什么不用字符串拼?因为括号关系天生就在结构里。如果存字符串 "3/4 ÷ (1/2 + 1/4)",那么「谁先算」要靠解析才知道;存成树之后:
- 求值=递归一趟,不会算错优先级;
- 判重=比较树形,不用去猜两个字符串是不是同一个题;
- 打印=按优先级决定加不加括号,规则只有两条。
3.3 关键流程
出题流程:
用户输入 -n 10 -r 10
└── main.generate()
├── generator.make_problems(10, 10)
│ └── 循环:expression.random_expression(10)
│ ├── build_tree(r, 运算符个数 1~3) ← 边造边验约束,不合规就重摇
│ ├── expression_key(tree) ← 规范化指纹
│ └── 指纹已在集合里?丢弃重摇;否则记下
└── generator.write_problems()
├── Exercises.txt: 1. 8 + 5/9 =
└── Answers.txt: 1. 8'5/9
判卷流程:
main.grade() → checker.check(题目文件, 答案文件)
└── 逐行:去掉行首题号
├── parse_tree(题目) → tree_value(树) ← 自己重算一遍标准答案
├── parse_fraction(答案) ← 支持 5、3/5、2'3/8
└── 相等 → 记入 Correct;否则/看不懂 → 记入 Wrong
└── Grade.txt: Correct: 8 (1, 2, 4, 5, 6, 8, 9, 10)
Wrong: 2 (3, 7)
判卷时是程序自己重算一遍答案,而不是去比对答案文件——这样即使答案文件被改坏(第 8 个用例就是这么测的),也能判出对错。
3.4 一个容易忽略的坑:打印括号的规则
生成时树是对的,但打印成文字时如果括号省错了,题面就变了。最典型的例子:
树:("×", 6, ("÷", 1/2, 4/7))
正确打印:6 × (1/2 ÷ 4/7) =
错误打印:6 × 1/2 ÷ 4/7 = ← 按从左到右算,(6×1/2) ÷ 4/7 = 21/4,已经不是真分数
所以 to_text 的规则定为:我的优先级比爹低(如 (1+2)×3)或者 我是右孩子且和爹同级(如 a-(b+c)、a÷(b×c)、6×(1/2÷4/7))时必须加括号;左孩子同级不用加,因为文字按同样的规则解析回来还是同一棵树。这条规则写成了测试用例,见第五节第 12 条。
四、代码说明
挑 4 段最关键的代码。完整代码见(仓库 3124004167/ 目录)。
4.1 判重:只交换 + 和 × 的左右,不重新结合
def expression_key(tree):
"""判重用的「规范化指纹」。
题目规定:+ 和 × 的左右两个操作数可以互换,互换后算同一个题目。
所以只要在每一层把 +、× 的两棵子树按同一套规则排好序,
两个等价题目的指纹就会完全一样,直接放进 set 就能去重。
"""
if len(tree) == 1:
return ("数值", tree[0])
op, left, right = tree
first, second = expression_key(left), expression_key(right)
if op in ("+", "×") and second < first:
first, second = second, first
return (op, first, second)
思路:作业原文说「任何两道题目不能通过有限次交换 + 和 × 左右的算术表达式变换为同一道题目」。这句话翻译成程序就是——把每个 +、× 节点的两棵子树排序,再看两棵树是否完全一样。于是等价题目得到同一个指纹,放进 set 就完成了去重,判重从「两两比较」变成了「查一次集合」。
注意这里故意不做「加法可以重新结合」的化简:1+2+3 与 3+2+1 的树形分别是 (1+2)+3 和 (3+2)+1,只交换左右的规则无法把一个变成另一个,所以它们不算重复;而 3+(2+1) 经过一次交换就变成 (1+2)+3,算重复。这和作业原文举的两个例子完全一致。
4.2 生成:一边造树一边守三条约束
def build_tree(r, operators, tries=BUILD_TRIES):
"""随机造一棵运算符个数恰好为 operators 的表达式树;条件凑不齐返回 None。"""
if operators == 0:
value = random_operand(r)
return (value,), value
for _ in range(tries):
op = random.choice(OPERATORS)
left_count = random.randrange(operators) # 0 ~ operators-1 个
right_count = operators - 1 - left_count
left = build_tree(r, left_count, tries)
if left is None:
continue
right = build_tree(r, right_count, tries)
if right is None:
continue
left_tree, left_value = left
right_tree, right_value = right
if op == "-":
if left_value < right_value: # 交换左右,保证 e1 >= e2
left_tree, right_tree = right_tree, left_tree
left_value, right_value = right_value, left_value
elif op == "÷" and not 0 < left_value < right_value:
continue # 要求“被除数 > 0 且 商 < 1”,也就是商必须是真分数
if r >= TRIVIAL_GUARD and not TRIVIAL_RULESop:
continue # 范围够大,就不要 “× 1”“+ 0” 这种没营养的位置
return (op, left_tree, right_tree), apply_op(op, left_value, right_value)
return None
思路:运算符的个数由递归参数 operators 决定(最多 3 个),左右子树的运算符个数随机分配。每造完一个节点就检查:
- 减法:要求
e1 >= e2,所以left < right时直接把左右子树换过来——这比「重摇」划算得多; - 除法:要求商是真分数,也就是
0 < 被除数 < 除数,不满足就重摇这个节点(顺便排除了除数为 0); - 出不来就返回
None,让上一层再试;试满tries次还是不行就往上传,最终由make_problems决定是继续还是收工。
4.3 判卷:自己重算一遍答案
correct, wrong = [], []
for index, (exercise, answer) in enumerate(zip(exercises, answers), start=1):
try:
expected = evaluate_text(_strip_index(exercise).rstrip("="))
given = parse_fraction(_strip_index(answer))
except (ValueError, ZeroDivisionError):
wrong.append(index) # 抄错或者看不懂的答案,一律算错
continue
if expected == given:
correct.append(index)
else:
wrong.append(index)
with open(grade_file, "w", encoding="utf-8") as handle:
handle.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
handle.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
思路:题目文件里只有算式(如 3. 8 - (4 + (2 + 1/2)) =),所以判卷时把算式解析成树、重新算一遍标准答案,再和学生答案比。学生答案允许三种写法:5、3/5、2'3/8(中文排版的 2’3/8 也认)。行首题号、多余空格、行尾等号都用正则或解析器顺手去掉,这样别人交来的格式略有差异也能判。输出格式严格按作业要求:Correct: 8 (1, 2, 4, 5, 6, 8, 9, 10)。
4.4 命令行:-r 不给就报错并给帮助信息
def generate(args, parser):
"""生成题目模式。"""
if args.r is None:
print("错误:必须用 -r 参数指定数值范围,例如 python3 main.py -n 10 -r 10")
parser.print_help()
return 1
if args.r < 1:
print("错误:-r 必须是大于等于 1 的自然数")
return 1
count = DEFAULT_COUNT if args.n is None else args.n
if count < 1:
print("错误:-n 必须是大于等于 1 的自然数")
return 1
思路:作业要求「-r 必须给定,否则程序报错并给出帮助信息」,正好对应 Python 的 argparse:-r 不设默认值,缺了就自己打印一句人话错误 + parser.print_help(),并返回退出码 1(方便脚本判断)。-n 不填时默认生成 10 道题。另外 -e 和 -a 必须成对出现,只给一个也会报错并提示正确用法。
五、测试运行
5.1 自动化测试
35 个单元测试,一条命令跑完:
python3 -m unittest discover -s tests -v
结果:
Ran 35 tests in 0.364s
OK
5.2 12 个测试用例
| 编号 | 测试内容 | 输入(命令 / 操作) | 预期结果 | 实际结果 |
|---|---|---|---|---|
| 1 | 基本生成 | python3 main.py -n 10 -r 10 |
两个文件各 10 行,题面以 n. 算式 = 结尾 |
通过,见 5.3 |
| 2 | 只给 -r 时默认道数 |
python3 main.py -r 10 |
生成 10 道题 | 通过:共生成 10 道题目 |
| 3 | 缺 -r 报错 |
python3 main.py -n 10 |
报错 + 帮助信息,退出码 1 | 通过,见 5.3 |
| 4 | -r 非法 |
python3 main.py -n 10 -r 0 |
提示范围必须 ≥ 1,退出码 1 | 通过 |
| 5 | -n 非法 |
python3 main.py -n 0 -r 10 |
提示个数必须 ≥ 1,退出码 1 | 通过 |
| 6 | 判卷参数不全 | python3 main.py -e Exercises.txt |
提示 -e、-a 要成对使用,退出码 1 |
通过 |
| 7 | 全部答对 | 用刚生成的 Answers.txt 判卷 |
Correct: 10 (1, 2, ..., 10) / Wrong: 0 () |
通过 |
| 8 | 部分答错 | 样例目录里故意改错第 3、7 题 | Correct: 8 (...) / Wrong: 2 (3, 7) |
通过,见 5.3 |
| 9 | 判卷容错 | 题面多打空格、撇号写成中文 ’ |
仍然判对 | 通过(test_grade_tolerates_spaces_and_chinese_apostrophe) |
| 10 | 约束:不出现负数 | 生成 300 道题逐题检查每个减法子表达式 | 全部 e1 >= e2 |
通过 |
| 11 | 约束:除法商是真分数 | 同上,检查每个除法 | 全部满足 0 < 被除数 < 除数 |
通过 |
| 12 | 约束:运算符 ≤ 3 且括号不丢 | 生成 300 道题,把题面解析回树再求值 | 题面能算出答案文件里的值;6 × (1/2 ÷ 4/7) 这类括号必须保留 |
通过 |
| 13 | 判重规则 | 直接比较 3+(2+1)、1+2+3、3+2+1、1×2、2×1 的指纹 |
前两个相同;3+2+1 与它们不同;1×2 与 2×1 相同 |
通过 |
| 14 | 小范围不崩 | python3 main.py -n 5 -r 1、-r 2 |
正常生成不报错 | 通过 |
| 15 | 凑不出时收工 | python3 main.py -n 10000 -r 1 |
提示范围太小,给出实际生成的题数,不死循环 | 通过:只生成 84 道并给出提示 |
| 16 | 一万道题 | python3 main.py -n 10000 -r 100 |
一万行、秒级完成 | 通过:0.20 秒,见 5.3 |
5.3 实际运行记录
生成 10 道 10 以内的题目:
$ python3 main.py -n 10 -r 10
题目已写入 Exercises.txt,答案已写入 Answers.txt
共生成 10 道题目,用时 0.00 秒
Exercises.txt:
- 8 + 5/9 =
- 2/3 - 1/3 ÷ 3/4 =
- 8 - (4 + (2/3 + 1/2)) =
- 6 - (1/2 + 2/3) =
- 6 + 1/7 - (4 - 1/2) =
- 1/2 ÷ 2/3 =
- 1/6 × (1/2 - (6/7 - 3/5)) =
- 2/5 × 1/2 + 3/4 =
- 1/4 × 8 =
- (3 + 2/9) ÷ 9 + 1/3 =
Answers.txt:
- 8'5/9
- 2/9
- 2'5/6
- 4'5/6
- 2'9/14
- 3/4
- 17/420
- 19/20
- 2
- 56/81
判卷(样例里故意答错第 3、7 题):
$ python3 main.py -e 样例/Exercises.txt -a 样例/我的答案.txt
判卷完成,结果已写入 Grade.txt
正确 8 题,错误 2 题
Grade.txt:
Correct: 8 (1, 2, 4, 5, 6, 8, 9, 10)
Wrong: 2 (3, 7)
一万道题:
$ python3 main.py -n 10000 -r 100
题目已写入 Exercises.txt,答案已写入 Answers.txt
共生成 10000 道题目,用时 0.20 秒
缺 -r 时报错并给出帮助信息:
$ python3 main.py -n 10
错误:必须用 -r 参数指定数值范围,例如 python3 main.py -n 10 -r 10
usage: Myapp [-h] [-n 数量] [-r 范围] [-e 题目文件] [-a 答案文件]
自动生成小学四则运算题目的命令行程序
-n 数量 生成题目的个数(不填默认 10)
-r 范围 题目中数值(自然数、真分数和真分数分母)的范围,必须给定
-e 题目文件 待批改的题目文件,需与 -a 一起使用
-a 答案文件 待批改的答案文件,需与 -e 一起使用
(退出码 1)
5.4 为什么能确定程序是正确的
- 答案不是「看着对」,而是能互相验证:每道题都把题面文字重新解析成树、再算一遍,必须等于答案文件里的答案(用例 12)。这一条同时验证了「求值正确」和「括号没丢」,一旦打印括号的规则写错,这一条立刻报红。
- 约束是逐条检查的,不是抽查:负数、除法商不是真分数、运算符超过 3 个、操作数超出
-r范围,都在 300 道题上逐题验证过。 - 判重规则有作业原文的两个例子做锚点:
3+(2+1)与1+2+3必须判为重复,3+2+1与1+2+3必须判为不同——这是原文写明的,程序的行为与之一致(用例 13)。另外还检查了题目文件里没有两行完全相同的算式。 - 边界与异常都测了:
-r 1、-r 2、-n 10000 -r 1(凑不齐)、缺参数、参数非法、文件不存在、答案文件被改坏——不崩、不死循环、给得出人话提示。 - 有理数运算本身精确:内部全部用
fractions.Fraction,不存在浮点误差,所以「答案对不对」这件事是确定的,不是近似的。
六、PSP 表格(实际耗时)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 45 |
| · Estimate | · 估计这个任务需要多少时间 | 20 | 25 |
| Development | 开发 | 400 | 520 |
| · Analysis | · 需求分析(包括学习新技术) | 60 | 90 |
| · Design Spec | · 生成设计文档 | 30 | 25 |
| · Design Review | · 设计复审(和同事审核设计文档) | 20 | 30 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 15 | 20 |
| · Design | · 具体设计 | 45 | 50 |
| · Coding | · 具体编码 | 150 | 300 |
| · Code Review | · 代码复审 | 30 | 40 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 50 | 85 |
| Reporting | 报告 | 90 | 95 |
| · Test Report | · 测试报告 | 30 | 35 |
| · Size Measurement | · 计算工作量 | 15 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 45 | 45 |
| 合计 | 520 | 780 |
实际比预估多了 140 分钟,多出来的部分几乎都在「具体编码」和「测试」两栏:括号打印规则和判重规则各返工了一次(见下面小结)。
七、项目小结
7.1 做得好的地方
- 先把要求翻译成清单,再动手写代码。 需求里的「不能产生负数」「除法的结果必须是真分数」「运算符不超过 3 个」「不能重复」这几条,都是先写成测试检查项,再去写生成逻辑,避免了「先写出来再说」。
- 表达式用树表示,一次性解决了三个问题。 求值不用管优先级、判重不用比字符串、打印括号有明确规则。中途虽然因为树的打印规则返工过一次,但整体上这个选择省下了大量调试时间。
- 判卷独立重算答案。 不去信任答案文件,而是自己算一遍,所以答案文件被改坏、格式不统一都能处理。
- 性能不是「感觉快」,而是有数据。 用
cProfile采样,知道build_tree是热点,也知道判重改成集合到底快了多少(3000 道题从 152 毫秒降到 5.2 毫秒)。 - 一万道题真的跑得动:
-n 10000 -r 100实测 0.2 秒,满足需求 9。
7.2 踩过的坑
- 括号省过头,题面就变了。 最初打印时只在「优先级低于父节点」时加括号,结果
6 × (1/2 ÷ 4/7)被印成6 × 1/2 ÷ 4/7,按从左到右算那一步的商不是真分数。改成「右孩子同级必须加括号」后,并且加了一条测试用例专门盯这个形状。 - 判重一开始想复杂了。 曾打算把加减乘除的等价形式都展开(甚至考虑过乘法分配律),后来回到作业原文:只允许交换
+、×的左右。规则简单了,实现也从「两两比较」变成了「查集合」。 - 浮点数差点埋雷。 早期用
float存分数,1/6 + 1/8会算出0.29166666666666663,输出格式也不对。换成Fraction之后,答案永远是最简分数,格式转换只用处理「整数/真分数/带分数」三种情况。 - 范围太小的极端输入。
-n 10000 -r 1时只有 0 能用,理论上凑不出 10000 道互不重复的题目。如果不设上限,程序会一直转。最后改成「连续 20000 次摇不出新题就收工」,并在终端说明实际生成了多少道(84 道)。
7.3 结对感受
说明:下面这段需要两人一起补充真实感受,这里先给出框架,替换成自己的话再提交。
两人的分工大致是:一人主攻「表达式生成 + 判重」,另一人主攻「文件读写 + 判卷 + 测试」,接口先定好(随机生成一道题 → (题面文字, 答案文字)、判卷(题目文件, 答案文件) → 对错题号),之后各自写各自的模块,每完成一块就互相读一遍代码。
感受:两个人一起看代码,确实比一个人看得清楚。 上面第 1 个坑(括号)就是互查时发现的——自己写的时候一直觉得「不加括号也差不多」,对方拿作业原文一对,立刻发现那一步的商已经不是真分数了。此外「一个人写、一个人盯需求清单」的方式很有效,避免写着写着漏掉某条要求。
7.4 对彼此的闪光点和建议
- 队友的闪光点:对项目的需求拆解细致、测试用例设计得很全面。
- 我给队友的建议:接口可以先写注释再去实现、提交注释要写清楚改了什么。
- 队友对我的建议:建议我动手写生成逻辑之前,先把边界情况(比如 -r 1、-n 10000)列成清单再写,免得写完才发现题目凑不出来;另外建议我每次提交前先自己跑一遍出题和判卷流程,确认推上去的代码是能跑的。
7.5 经验与教训
一次能跑通的代码不是终点,「每条要求都有对应用例守着」才是。
这次真正省时间的两个决定,一个是先用 Fraction 把「答案正确」变成确定的事,另一个是把判重从 O(n²) 换成集合。
相反的教训是:打印格式这类「看起来无关紧要」的地方最容易出错,因为错了程序也不报错,只是题目悄悄变了意思——所以给它单独写了一个测试用例。
浙公网安备 33010602011771号