简化数学表达式的策略

我有一个结构良好的树,代表一个数学表达式。例如,给定字符串: "1+2-3*4/5",这将被解析为:

subtract(add(1,2),divide(multiply(3,4),5))

表示为这棵树:

"1+2-3*4/5"

我希望能够做的是采取这棵树,并尽可能减少它。在上面的例子中,这非常简单,因为所有的数字都是常量。然而,一旦我考虑到未知数(用 $表示,后面跟着一个标识符) ,事情就变得棘手起来:

"3*$a/$a"变成 divide(multiply(3,$a), $a)

这应该简化为 3,因为 $a术语应该相互抵消。问题是,“我该如何以一种通用的方式来识别它?”我如何认识到 min(3, sin($x))总是 sin($x)?我如何识别 sqrt(pow($a, 2))abs($a)?如何识别 nthroot(pow(42, $a), $a)(42的 a这个根的 a这个次方)是 42

我知道这个问题很宽泛,但我已经绞尽脑汁想了一段时间了,还没有想出任何令人满意的答案。

26458 次浏览

这个任务可能会变得非常复杂(除了最简单的转换之外)。

您可以找到一个可读的介绍如何做到这一点(基于规则的评估) ,例如对于 Mathematica

你实际上不能这样做,因为,虽然它们在数学上是相同的,但在计算机算术中可能不是相同的。例如,-MAX _ INT 是未定义的,所以——% a =/=% a。类似地,对于浮点数,必须适当地处理 NaN 和 Inf。

您想要构建一个 CAS (计算代数系统) ,这个主题非常广泛,以至于有一整个领域专门研究它。这意味着有一些 可能会比 SO 更好地回答你的问题。

我知道有些系统构建树来首先减少常量,然后将树放入规范化形式,然后使用 已证明/已知公式的大型数据库将问题转换为其他形式。

我的初步方法是使用某种数据结构,其中每个函数都是倒数(即 dividemultiply)。显然,您需要进一步的逻辑来确保它们实际上是逆的,因为乘以3然后除以4实际上并不是逆的。

虽然这是原始的,但我认为这是解决这个问题的一个不错的第一步,并且可以解决你在问题中提到的很多情况。

我很期待看到你的完整解决方案,敬畏地凝视着数学的辉煌:)

我相信你必须“蛮力”这样的树。

您将不得不制定一些规则来描述可能的简化。然后您必须遍历树并搜索适用的规则。由于一些简化可能导致比其他更简单的结果,你将不得不做一个类似的事情,你找到一个地铁计划的最短路线: 尝试所有的可能性,并按照一些质量标准排序的结果。

由于这种情况的数量是有限的,你可以通过尝试所有操作符和变量的组合来自动发现简化规则,并再次使用一个遗传算法来验证这个规则以前没有被发现,并且它实际上简化了输入。

乘法可以表示为加法,所以一个规则可能是 a-a 相互抵消: 2a-a = a + a-a

另一个规则是首先将所有除法乘出,因为它们是分数。例如:

1/2 + 3/4 发现所有的除数,然后将每个分数与所有其他分数的两边的除数相乘

4/8 + 6/8 那么所有的元素都有相同的除数,因此统一到 (4 + 6)/8 = 10/8

最后,您可以找到顶部和底部之间的最高公约数 5/4

应用到你的树上,策略是从底部的叶子开始向上,通过将乘法转换为加法来简化每个乘法。然后像分数一样简化每个加法

与此同时,您将检查您的快捷规则,以发现这种简化。要知道规则是否适用,您可能需要尝试子树的所有排列。例如。A-a 规则也适用于-a + a。可能是 a + b-a。

只是一些想法,希望能给你一些想法..。

你可能想实现一个 术语重写系统术语重写系统。关于底层的数学,看看 维基百科

术语重写模块的结构

因为我最近实施了一个解决方案..。

  • 首先,准备一个类 CExpression,它为表达式的结构建模。

  • 实现 CRule,它包含一个模式和一个替换。使用特殊的符号作为模式变量,这些变量需要在模式匹配中被绑定并在替换表达式中被替换。

  • 然后,实现一个类 CRule。它的主方法 applyRule(CExpression, CRule)尝试将规则与任何适用的表达式子表达式匹配。如果匹配,则返回结果。

  • 最后,实现一个类 CRuleSet,它只是一组 CRule 对象。只要不能应用更多的规则,主方法 reduce(CExpression)就应用这组规则,然后返回约简后的表达式。

  • 此外,还需要一个类 CBindingEnvironment,它将已经匹配的符号映射到匹配的值。

尝试将表达式重写为正常形式

不要忘记,这种方法在一定程度上有效,但可能是不完整的。这是由于以下所有规则都执行本地术语重写。

为了使这种局部重写逻辑更强大,应该尝试将表达式转换为我称之为标准形式的形式。这是我的方法:

  • 如果一个术语包含文字值,尝试将该术语尽可能向右移动。

  • 最终,这个文字值可能出现在最右边,并且可以作为完全文字表达式的一部分进行计算。

何时计算完全文字表达式

一个有趣的问题是什么时候计算完全文字表达式

   x * ( 1 / 3 )

那就会变成

   x * 0.333333333333333333

现在假设 x 被3代替

   0.999999999999999999999999

因此即时计算返回一个稍微不正确的值。

在另一边,如果你保持(1/3)并且先用3代替 x

   3 * ( 1 / 3 )

重写规则

   1

因此,在后期计算完全文字表达式可能是有用的。

重写规则的示例

下面是我的规则在应用程序中的显示方式: _ 1,_ 2,... 符号匹配任何子表达式:

addRule( new TARuleFromString( '0+_1',   // left hand side  :: pattern
'_1'      // right hand side :: replacement
)
);

或者更复杂一点

addRule( new TARuleFromString( '_1+_2*_1',
'(1+_2)*_1'
)
);

某些特殊符号只匹配特殊的子表达式。例如 _ Literal1,_ Literal2,... ... 只匹配文字值:

addRule( new TARuleFromString( 'exp(_Literal1) * exp(_Literal2 )',
'exp( _Literal1 + _Literal2 )'
)
);

此规则将非字面表达式移到左边:

addRule( new TARuleFromString( '_Literal*_NonLiteral',
'_NonLiteral*_Literal'
)
);

任何以“ _”开头的名称都是一个模式变量。当系统匹配一个规则时,它保留一堆已经匹配的符号。

最后,不要忘记规则可能产生非终止替换序列。 因此,在减少表达式的同时,让进程记住以前已经到达的中间表达式。

在我的实现中,我不直接保存中间表达式,而是保存中间表达式的 MD5()散列数组。

作为起点的一组规则

这里有一套开始的规则:

            addRule( new TARuleFromString( '0+_1', '_1' ) );
addRule( new TARuleFromString( '_Literal2=0-_1', '_1=0-_Literal2' ) );
addRule( new TARuleFromString( '_1+0', '_1' ) );
            

addRule( new TARuleFromString( '1*_1', '_1' ) );
addRule( new TARuleFromString( '_1*1', '_1' ) );
            

addRule( new TARuleFromString( '_1+_1', '2*_1' ) );
            

addRule( new TARuleFromString( '_1-_1', '0' ) );
addRule( new TARuleFromString( '_1/_1', '1' ) );
            

// Rate = (pow((EndValue / BeginValue), (1 / (EndYear - BeginYear)))-1) * 100


addRule( new TARuleFromString( 'exp(_Literal1) * exp(_Literal2 )', 'exp( _Literal1 + _Literal2 )' ) );
addRule( new TARuleFromString( 'exp( 0 )', '1' ) );
            

addRule( new TARuleFromString( 'pow(_Literal1,_1) * pow(_Literal2,_1)', 'pow(_Literal1 * _Literal2,_1)' ) );
addRule( new TARuleFromString( 'pow( _1, 0 )', '1' ) );
addRule( new TARuleFromString( 'pow( _1, 1 )', '_1' ) );
addRule( new TARuleFromString( 'pow( _1, -1 )', '1/_1' ) );
addRule( new TARuleFromString( 'pow( pow( _1, _Literal1 ), _Literal2 )', 'pow( _1, _Literal1 * _Literal2 )' ) );


//          addRule( new TARuleFromString( 'pow( _Literal1, _1 )', 'ln(_1) / ln(_Literal1)' ) );
addRule( new TARuleFromString( '_literal1 = pow( _Literal2, _1 )', '_1 = ln(_literal1) / ln(_Literal2)' ) );
addRule( new TARuleFromString( 'pow( _Literal2, _1 ) = _literal1 ', '_1 = ln(_literal1) / ln(_Literal2)' ) );


addRule( new TARuleFromString( 'pow( _1, _Literal2 ) = _literal1 ', 'pow( _literal1, 1 / _Literal2 ) = _1' ) );
            

addRule( new TARuleFromString( 'pow( 1, _1 )', '1' ) );


addRule( new TARuleFromString( '_1 * _1 = _literal', '_1 = sqrt( _literal )' ) );
            

addRule( new TARuleFromString( 'sqrt( _literal * _1 )', 'sqrt( _literal ) * sqrt( _1 )' ) );
            

addRule( new TARuleFromString( 'ln( _Literal1 * _2 )', 'ln( _Literal1 ) + ln( _2 )' ) );
addRule( new TARuleFromString( 'ln( _1 * _Literal2 )', 'ln( _Literal2 ) + ln( _1 )' ) );
addRule( new TARuleFromString( 'log2( _Literal1 * _2 )', 'log2( _Literal1 ) + log2( _2 )' ) );
addRule( new TARuleFromString( 'log2( _1 * _Literal2 )', 'log2( _Literal2 ) + log2( _1 )' ) );
addRule( new TARuleFromString( 'log10( _Literal1 * _2 )', 'log10( _Literal1 ) + log10( _2 )' ) );
addRule( new TARuleFromString( 'log10( _1 * _Literal2 )', 'log10( _Literal2 ) + log10( _1 )' ) );


addRule( new TARuleFromString( 'ln( _Literal1 / _2 )', 'ln( _Literal1 ) - ln( _2 )' ) );
addRule( new TARuleFromString( 'ln( _1 / _Literal2 )', 'ln( _Literal2 ) - ln( _1 )' ) );
addRule( new TARuleFromString( 'log2( _Literal1 / _2 )', 'log2( _Literal1 ) - log2( _2 )' ) );
addRule( new TARuleFromString( 'log2( _1 / _Literal2 )', 'log2( _Literal2 ) - log2( _1 )' ) );
addRule( new TARuleFromString( 'log10( _Literal1 / _2 )', 'log10( _Literal1 ) - log10( _2 )' ) );
addRule( new TARuleFromString( 'log10( _1 / _Literal2 )', 'log10( _Literal2 ) - log10( _1 )' ) );
            

        

addRule( new TARuleFromString( '_Literal1 = _NonLiteral + _Literal2', '_Literal1 - _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal1 = _NonLiteral * _Literal2', '_Literal1 / _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal1 = _NonLiteral / _Literal2', '_Literal1 * _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal1 =_NonLiteral - _Literal2',  '_Literal1 + _Literal2 = _NonLiteral' ) );


addRule( new TARuleFromString( '_NonLiteral + _Literal2 = _Literal1 ', '_Literal1 - _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_NonLiteral * _Literal2 = _Literal1 ', '_Literal1 / _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_NonLiteral / _Literal2 = _Literal1 ', '_Literal1 * _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_NonLiteral - _Literal2 = _Literal1',  '_Literal1 + _Literal2 = _NonLiteral' ) );
            

addRule( new TARuleFromString( '_NonLiteral - _Literal2 = _Literal1 ', '_Literal1 + _Literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal2 - _NonLiteral = _Literal1 ', '_Literal2 - _Literal1 = _NonLiteral' ) );
            

addRule( new TARuleFromString( '_Literal1 = sin( _NonLiteral )', 'asin( _Literal1 ) = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal1 = cos( _NonLiteral )', 'acos( _Literal1 ) = _NonLiteral' ) );
addRule( new TARuleFromString( '_Literal1 = tan( _NonLiteral )', 'atan( _Literal1 ) = _NonLiteral' ) );


addRule( new TARuleFromString( '_Literal1 = ln( _1 )', 'exp( _Literal1 ) = _1' ) );
addRule( new TARuleFromString( 'ln( _1 ) = _Literal1', 'exp( _Literal1 ) = _1' ) );
            

addRule( new TARuleFromString( '_Literal1 = _NonLiteral', '_NonLiteral = _Literal1' ) );


addRule( new TARuleFromString( '( _Literal1 / _2 ) = _Literal2', '_Literal1 / _Literal2 = _2 ' ) );
            

addRule( new TARuleFromString( '_Literal*_NonLiteral', '_NonLiteral*_Literal' ) );
addRule( new TARuleFromString( '_Literal+_NonLiteral', '_NonLiteral+_Literal' ) );
            

addRule( new TARuleFromString( '_Literal1+(_Literal2+_NonLiteral)', '_NonLiteral+(_Literal1+_Literal2)' ) );
addRule( new TARuleFromString( '_Literal1+(_Literal2+_1)', '_1+(_Literal1+_Literal2)' ) );


addRule( new TARuleFromString( '(_1*_2)+(_3*_2)', '(_1+_3)*_2' ) );
addRule( new TARuleFromString( '(_2*_1)+(_2*_3)', '(_1+_3)*_2' ) );


addRule( new TARuleFromString( '(_2*_1)+(_3*_2)', '(_1+_3)*_2' ) );
addRule( new TARuleFromString( '(_1*_2)+(_2*_3)', '(_1+_3)*_2' ) );
            

addRule( new TARuleFromString( '(_Literal * _1 ) / _Literal', '_1' ) );
addRule( new TARuleFromString( '(_Literal1 * _1 ) / _Literal2', '(_Literal1 * _Literal2 ) / _1' ) );
            

addRule( new TARuleFromString( '(_1+_2)+_3', '_1+(_2+_3)' ) );
addRule( new TARuleFromString( '(_1*_2)*_3', '_1*(_2*_3)' ) );


addRule( new TARuleFromString( '_1+(_1+_2)', '(2*_1)+_2' ) );


addRule( new TARuleFromString( '_1+_2*_1', '(1+_2)*_1' ) );


addRule( new TARuleFromString( '_literal1 * _NonLiteral = _literal2', '_literal2 / _literal1 = _NonLiteral' ) );
addRule( new TARuleFromString( '_literal1 + _NonLiteral = _literal2', '_literal2 - _literal1 = _NonLiteral' ) );
addRule( new TARuleFromString( '_literal1 - _NonLiteral = _literal2', '_literal1 - _literal2 = _NonLiteral' ) );
addRule( new TARuleFromString( '_literal1 / _NonLiteral = _literal2', '_literal1 * _literal2 = _NonLiteral' ) );

使规则成为一流的表达式

有趣的是: 由于上述规则是特殊的表达式,表达式解析器可以正确地计算这些规则,因此用户甚至可以添加新的规则,从而增强应用程序的重写能力。

解析表达式(或更一般的语言)

对于 Cocoa/OBjC 应用程序Dave DeLong 的 DDMathParser是句法分析数学表达式的理想候选者。

对于其他语言,我们的老朋友 Lex & Yacc或更新的 GNU Bison可能会有所帮助。

ANTLR是一款基于 Java 的现代编译器编译程式。除了纯粹使用命令行之外,ANTLRWorks还提供了一个 图形用户界面前端来构造和调试基于 ANTLR 的解析器。ANTLR 为 各种主机语言生成语法,如 JAVA,C,Python,PHP 或 C # 。ActionScript 运行时当前为 支离破碎

如果您希望从下往上学习 学习如何解析表达式(或一般的语言) ,我建议使用 来自 Niklaus Wirth 的免费书籍文本(或 德国书籍版本) ,它是帕斯卡和模块2的著名发明者。