背景
从产品角度看,K12应用题批改是应用题求解的进一步功能拓展,正确批改建立在应用题正确求解的基础之上。 本文用于记录在实现求解后的应用题批改的设计与实现。
问题定义
批改范围定义
应用题求解输入存在两种情况:
- 应用相同的公式,即求解结果相同,但列式的顺序或组合不同
- 应用不同的公式求解,即求解结果相同,不同求解方法的的列式也不同
从实现的逻辑上看,情况2其实可由不同解法输入的情况1求解获得,因此解决情况1即可生成应用题的所有求解(批改标准)
关键问题
(1)如何通过最简求解算式集合判断学生不同形式、顺序的求解算式集合正确与否?
因为批改的最简求解算式集合是确定的,而学生求解的算式集合形式是不确定的,举例如下。
求解的最简集合为:
a. 合并算式(a + b)* (d + e)= g
b. 分解算式 a + b = c;d + e = f;c * f = g
而学生求解可能为:
a. 部分合并,顺序不同,如a+b=c;c * (d + e)= g 或 d+e=f ;(a + b)* f = g
b. 四则运算法则应用不同,或数字的表示形式不同,如a是b的一半可表示为a = b * 0.5,也可以表示为a = b * 1/2
【注意】乘除变换待定,如 3 / 5 => 3 xx 1/5
(2)批改匹配算法设计:目前实现为方案设计(1)
设计与实现
已否定的方案
根据算式直接比较的方案(忽略算式上下文,即单纯比较算式):
- 应用最简式直接循环匹配用户输入式子,如
| 匹配序号 | 最简求解式集合 | 用户可能的输入算式 |
|---|---|---|
| 初始输入 | a + b = c;d + e = f;c * f = g | (a + b)* (d + e) = g |
| 1 | d + e = f;c * f = g | c * (d + e) = g |
| 2 | c * f = g | c * f = g |
无法处理得数相同,意义不同的算式冲突问题,【舍弃】。具体举例为:(1+2)/(1+2), 两个1+2的含义不同,应用的公式也不同。
2. 根据用户输入纯数学解题算式集合反推最简式集合
因用户输入算式无上下文,同样无法处理上文指出的问题。
方案设计
(1)根据生成最简式集合生成上下文相关的所有求解路径,暂定方案
节点(参数)与算式关系:
区分两种情况,左为已知一参数(h)的情况,右为两个参数都要求解而得的情况
生成算式路径集(对应上图)
数据结构设计
算法实现
由上观察可得需要记录每个算式的参数的前继算式,通过参数变换即可求得所有的变换式(抽象为算法实际是回溯生成全路径问题)
// 根据求解式的生成逻辑,求解式集必然是拓扑顺序输出算式,所以可以直接for循环求解
_根据求解式集生成全批改路径(求解式集) {
const 对象map = new Map()
for (let 求解式 of 求解式集) {
if (求解式的参数是通过另一求解式求得) {
// 每种求解进行一次回溯
for (let m of part) {
// 合并求解变换方程(区分“生成算式路径集”的左右两种情况)
let base = new Map()
for (let [key, obj] of m.entries()) {
base.set(key, obj)
}
// 回溯替换 求解式的参数 求解
dps(0, res, base, 求解式)
}
对象map.set(求解式.待求参数实例, res)
} else {
// 设置变换集
对象map.set(求解式.待求参数实例, [对象变换map])
}
}
// map转求解数组
const 批改路径集 = 对象map.get(last).map(m => [...m].map(([key, val]) => val))
return 批改路径集
}
(2)根据用户输入算式及题目上下文,反推所有算式的元素组成,即可根据用户输入算式反推 参数关系集,而后通过与解题得到的最简式集合比较判断
【注】上下文相关,即参数含有意义。如“1分钟“ 是某算式“时间 * 速度 = 路程” 的时间
存在问题
- 算法优化:
a. 在生成算式时判断该表达式能否进行乘除变换、数字表示形式变换、四则运算法则变换
b. 在生成可能的算式时生成全变换(效率很低)进行匹配 - 存在单位转换时,求解式子(表示形式)将完全不同,需要对单位换算进行判别和优化
- 合成算式时,存在计算优先级(乘除、加减),如何根据不同情况添加括号 并与用户输入进行比较?再生成运算树?