阅读 111

Mathjs表达式化简模块探索记录

背景

在求解K12数学应用题/运算题,生成运算/讲解步骤时,应用到多种化简表达式的方法(四则运算的分配律、交换律等),这里需要处理两个核心问题:

  1. 如何设计表达式化简逻辑(与表达式的数据结构强相关)

  2. 如何判断使用各化简方法的优先级

此文用以探索知名度、活跃度高的数学表达式解析库 math.js的实现

math.js 化简功能的设计与实现

src/function/algebra/simplify.js

表达式的数据结构

公认的数学表达式数据结构表示方式为树形结构(链表),通过将表达式解析为以二元运算符、数字、括号为节点存储(TODO: 此处需要添加一篇详解), 如math.js 解析字符串x + (y * 3)的树形结构为

import * as math from 'mathjs' const expression = 'x + (y * 3)'; const node = math.parse(expression)  /* peratorNode {     args:[         // SymbolNode         {name: 'x'},         // ParenthesisNode         {             content: {                 args: [                     // SymbolNode                     {name: 'y'},                     // ConstantNode                     {value: 3}                 ],                 fn: 'multiply',                 implicit: false,                 isPercentage: false,                 op: '*'             }         }     ],     comment:'',     fn:'add',     implicit:false,     isPercentage:false,     op:'+' } */ 复制代码

默认化简规则

可以通过调用以下代码即可打印内置的默认规console.log(math.simplify.rules) 或者 直接查看src/function/algebra/simplify.js, 237行

可见,math.js以对象左式l 和 右式r的转换字符串表示转换 简化规则({l:'', r:''})

import * as math from "../src/index.js" console.log(math.simplify.rules) /*   simplify.rules = [     simplifyCore,     // { l: 'n+0', r: 'n' },     // simplifyCore     // { l: 'n^0', r: '1' },     // simplifyCore     // { l: '0*n', r: '0' },     // simplifyCore     // { l: 'n/n', r: '1'},      // simplifyCore     // { l: 'n^1', r: 'n' },     // simplifyCore     // { l: '+n1', r:'n1' },     // simplifyCore     // { l: 'n--n1', r:'n+n1' }, // simplifyCore     { l: 'log(e)', r: '1' },     // temporary rules     { l: 'n-n1', r: 'n+-n1' }, // temporarily replace 'subtract' so we can further flatten the 'add' operator     { l: '-(c*v)', r: '(-c) * v' }, // make non-constant terms positive     { l: '-v', r: '(-1) * v' },     { l: 'n/n1^n2', r: 'n*n1^-n2' }, // temporarily replace 'divide' so we can further flatten the 'multiply' operator     { l: 'n/n1', r: 'n*n1^-1' },     // expand nested exponentiation     { l: '(n ^ n1) ^ n2', r: 'n ^ (n1 * n2)' },     // collect like factors     { l: 'n*n', r: 'n^2' },     { l: 'n * n^n1', r: 'n^(n1+1)' },     { l: 'n^n1 * n^n2', r: 'n^(n1+n2)' },     // collect like terms     { l: 'n+n', r: '2*n' },     { l: 'n+-n', r: '0' },     { l: 'n1*n2 + n2', r: '(n1+1)*n2' },     { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' },     // remove parenthesis in the case of negating a quantitiy     { l: 'n1 + -1 * (n2 + n3)', r: 'n1 + -1 * n2 + -1 * n3' },     simplifyConstant,     { l: '(-n)*n1', r: '-(n*n1)' }, // make factors positive (and undo 'make non-constant terms positive')     // ordering of constants     { l: 'c+v', r: 'v+c', context: { add: { commutative: false } } },     { l: 'v*c', r: 'c*v', context: { multiply: { commutative: false } } },     // undo temporary rules     // { l: '(-1) * n', r: '-n' }, // #811 added test which proved this is redundant     { l: 'n+-n1', r: 'n-n1' }, // undo replace 'subtract'     { l: 'n*(n1^-1)', r: 'n/n1' }, // undo replace 'divide'     { l: 'n*n1^-n2', r: 'n/n1^n2' },     { l: 'n1^-1', r: '1/n1' },     { l: 'n*(n1/n2)', r: '(n*n1)/n2' }, // '*' before '/'     { l: 'n-(n1+n2)', r: 'n-n1-n2' }, // '-' before '+'     // { l: '(n1/n2)/n3', r: 'n1/(n2*n3)' },     // { l: '(n*n1)/(n*n2)', r: 'n1/n2' },     { l: '1*n', r: 'n' }, // this pattern can be produced by simplifyConstant     { l: 'n1/(n2/n3)', r: '(n1*n3)/n2' }   ] */ 复制代码

基本应用

直接应用math.simplify()函数即可,具体如下所示

const math = require('mathjs'); const expression = 'x-3(x-2)'; const result = math.simplify(expression); console.log(result.toString()); // x - 3 * (x - 2) 复制代码

增加自定义简化规则(结合律)

默认化简规则可知,是没有提供结合律求解的,因此上文没能获得最简解,需要添加自定义的化简逻辑,具体如下所示

const math = require('mathjs'); const expression = 'x-3(x-2)'; const rules = [     {         l: 'n1*(n2+n3)',         r: 'n1*n2+n1*n3'     },     {         l: 'n1*(n2-n3)',         r: 'n1*n2-n1*n3'     } ];   const result = math.simplify(expression, math.simplify.rules.concat(rules)); console.log(result.toString()); // 6 - 2 * x 复制代码

化简顺序的缺陷

如果把表达式改为 x-3(x+y-2),会发现输出为x - (3 * x + 3 * y) + 6,而默认规则里是有{ l: 'n1 + -1 * (n2 + n3)', r: 'n1 + -1 * n2 + -1 * n3' },为什么规则没有生效(代码逻辑看下文化简逻辑)?因为算式化简是通过for循环遍历rules规则的,则应用规则的顺序是固定的。当前表达式不重复遍历重复的规则。

其实也可以通过简单函数测例,从解析顺序推理:当输入为 x+y-2时,parse函数将表达式解析成了 (x+y)-2, 所以x-3(x+y-2)处理过程如下:

x - ( 3 * ( ( x + y ) - 2) ) x - ( 3 * ( x + y ) - 3 * 2 ) x - ( ( 3 * x + 3 * y ) - 6 ) x - ( 3 * x + 3 * y ) + 6 复制代码

因此可以通过将新规则提前至新规则前,进行分配率操作:

const math = require('mathjs'); const expression = 'x+3(x+y-2)'; const rules = [     { l: 'n1*(n2+n3)', r: 'n1*n2+n1*n3' },     { l: 'n1*(n2-n3)', r: 'n1*n2-n1*n3' },     { l: '(n1+n2)*n3', r: 'n1*n2+n1*n3' },     { l: '(n1-n2)*n3', r: 'n1*n2-n1*n3' }, ];   let newRules = rules.concat(math.simplify.rules); let result = math.simplify(expression, newRules); console.log(result.toString()); // (y + 4) * x - 6 复制代码

按顺序化简,首先将x+3(x+y-2)进行分配律操作,而后在后续rules将(y * x + 4 * x - 6 合并为(y + 4) * x - 6。化简顺序和结果与rules遍历顺序强相关。

化简逻辑

可直接跳过,仅用于帮助理解其设计:

  1. parse(expression) 将表达式解析为树形结构 ->

  2. new simplify 新建化简对象-> generic.apply(fn, arguments) ->

  3. 'Node, Array, Map, Object': function (expr, rules, scope, options){}  执行简化逻辑

(1) 其中rules = _buildRules(rules) 同样将rules解析为以l和r区分的math.js树形结构。解析后的数据结构如下图所示: image.png (2)最后通过applyRule(res, rules[i])将化简规则应用进行节点替换

// 执行简化逻辑  // src/function/algebra/simplify.js, 208行    'Node, Array, Map, Object': function (expr, rules, scope, options) {       rules = _buildRules(rules)  // (1)将化简规则rules解析为math.js的树形结构       let res = resolve(expr, scope)        res = removeParens(res)        const visited = {}       let str = res.toString({ parenthesis: 'all' })       while (!visited[str]) {         visited[str] = true         _lastsym = 0 // counter for placeholder symbols         for (let i = 0; i < rules.length; i++) {           if (typeof rules[i] === 'function') {             res = rules[i](res, options)           } else {             flatten(res)             res = applyRule(res, rules[i]) // (2)应用化简规则           }           unflattenl(res) // using left-heavy binary tree here since custom rule functions may expect it         }         str = res.toString({ parenthesis: 'all' })       }       return res     }   }) // 应用化简规则 // src/function/algebra/simplify.js, 416行  const applyRule = typed('applyRule', {}) 复制代码

结论

通过以上的测例、代码分析可知,mathjs的化简步骤是按照化简规则遍历顺序执行的,以结果为导向。如果要获取详细的化简、求解过程,需要动态的选择规则进行匹配(applyRule)。
若应用mathjs用于获取详细的求解步骤,需要设计实现更详细的规则选取逻辑(如同时出现分配律结合律的情况应如何做选择?)


作者:了凡一生
链接:https://juejin.cn/post/7023292805502435358


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐