简化数学表达式的策略


72

我有一棵结构良好的树,代表数学表达式。例如,给定字符串:"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)(第a次方为42的a次方)是42

我知道这个问题涉及面很广,但是我已经对此提出质疑了很长时间,而且还没有提出令人满意的建议。


6
那又如何sqrt(pow(-3, 2))呢?
霍华德

1
@Howard的计算结果为3,这是为什么我还没有想到一个好的答案的一个示例(经过编辑的问题可以反映出来)。:)
Dave DeLong

8
3a/a与相同,3因为时3a/a未定义a=0
尼克·约翰逊

2
@NickJohnson有趣的提示!即使lim(a-> 0 | left,3 * a / a)= 3和lim(a-> 0 | right,3 * a / a),即使a = 0也不3 * a / a等于3 = 3?
SteAp 2011年

2
@StefanPantke否,因为对于所有x都未定义x / 0。一个函数(如该函数)可能具有一个点间断,即使在任一方向上的限制都是一致的。
尼克·约翰逊

Answers:


93

您可能想要实现术语重写系统。关于基础数学,请看WikiPedia

术语重写模块的结构

自从我最近实施解决方案以来...

  • 首先,准备一个CExpression类,该类对表达式的结构进行建模。

  • Implement CRule,其中包含一个模式和一个替换。使用特殊符号作为模式变量,在模式匹配期间需要绑定特殊符号,并在替换表达式中将其替换。

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

  • 最后,实现一个class CRuleSet,它只是一组CRule对象。main方法reduce(CExpression)将应用规则集,只要不能再应用其他规则,然后返回简化的表达式即可。

  • 另外,您需要一个class CBindingEnvironment,它将已经匹配的符号映射到匹配的值。

尝试将表达式重写为普通形式

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

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

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

  • 最终,此文字值可能会出现在最右边,并且可以作为完整文字表达式的一部分进行评估。

何时评估完全文字表达

一个有趣的问题是何时评估完全文字表达。假设您有一个表达式

   x * ( 1 / 3 )

这将减少到

   x * 0.333333333333333333

现在假设x被3代替。这将产生类似

   0.999999999999999999999999

因此,急切的评估会返回一个略有错误的值。

另一方面,如果您保留(1/3)并首先将x替换为3

   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还提供了一个GUI前端 来构造和调试基于ANTLR的解析器。ANTLR为各种宿主语言(如JAVA,C,Python,PHP或C#)生成语法。目前,ActionScript运行时已损坏

如果您想自下而上地学习如何解析表达式(或一般语言),我建议这本免费书籍的文字来自著名的Pascal和Modula发明家Niklaus Wirth(或德语版) -2。


4
+1这确实很有趣,并且是迄今为止最有前途的方法。
Dave DeLong

您提供的许多规则都是不必要的,因为我不与=操作员打交道,但这给了我很多建议。除非出现更好的情况,否则我将其标记为正确。
Dave DeLong

真正!我只是复制了所有Flex代码。顺便说一句:我的解决方案旨在解决方程组。用户将某些符号设置为值,然后应用程序将计算所有剩余的符号或证明系统不一致。
SteAp 2011年

或者,您只需获取一个包含术语重写系统的引擎并使用它。参见semdesigns.com/Products/DMS/SimpleDMSmainExample.html
Ira Baxter,

任何实际的解决方案都不能简单地无条件地反复应用规则。那sin(x)* sqrt(1 + cos(x)^ 2 /(sin(x)^ 2))呢?您的规则可以将事物从平方根分解出来(例如,它们会将sqrt(9 * 2)变成3 * sqrt(2)),但是它们永远不会将其散布回去,这是必需的: (x)^ 2 + cos(x)^ 2)-> sqrt(1)-> 1.一个好的解决方案必须能够双向发展,并确定使用AI样式的搜索树最有希望的简化路径。
markasoftware

12

该任务可能变得非常复杂(除了最简单的转换之外)。本质上,这就是代数软件一直在做的事情。

您可以找到一个可读的介绍,例如Mathematica的完成方式(基于规则的评估)。


2
当然,可以使用CAS软件来简化术语。但是,如果需要将术语重写到应用程序中,则标准CAS并非可行之路,因为大多数CAS是不可嵌入的。
SteAp 2012年

9

您想构建一个CAS(计算代数系统),并且话题如此广泛,以至于有一个专门研究整个领域的知识。这意味着有些可能比SO更好地回答您的问题。

我知道有些系统会构建树,这些树首先会减少常数,然后将树变成规范化的形式,然后使用经过验证/已知公式大型数据库将问题转换为其他形式。


2

相信您必须“蛮力”砍伐这些树木。

您将必须制定一些规则来描述可能的简化。然后,您就可以遍历这棵树并搜索适用的规则。由于某些简化方法可能会导致比其他方法更简单的结果,因此您必须执行与在地铁计划中查找最短路线相同的操作:尝试所有可能性,并按某些质量标准对结果进行排序。

由于这种情况的数量是有限的,因此您可以通过尝试运算符和变量的所有组合来自动发现简化规则,并再次使用遗传算法来验证以前未找到该规则,并且实际上简化了输入。

乘法可以表示为加法,因此一个规则可能是a-a自行抵消:2a-a = a + aa

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

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

4/8 + 6/8然后所有元素都具有相同的除数,因此可以统一为(4 + 6)/ 8 = 10/8

最后,您发现顶部和底部5/4之间的最高公约数

应用于您的树时,策略是从下叶向上进行工作,首先将乘法转换为加法,从而简化了每个乘法。然后像分数一样简化每个加法

一直以来,您都将再次检查您的快捷方式规则以发现这种简化。要知道规则适用,您可能必须尝试子树的所有排列。例如,aa规则也适用于-a + a。可能会有a + ba。

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


0

实际上,您通常不能这样做,因为尽管它们在数学上是相同的,但在计算机算法上可能并不相同。例如,-MAX_INT是未定义的,因此-%a = / =%a。同样,对于浮点数,您必须适当地处理NaN和Inf。


1
假设补码为2,则-MAX_INT为MIN_INT + 1,但-MIN_INT为MIN_INT。所以,是的--a = a,但并非总是以一种很好的方式(即,即使a!= 0,也存在一个使-a = a的a)
harold

0

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

尽管这很原始,但我认为这是解决该问题的不错的第一步,它将解决您在问题中提到的许多情况。

我确实希望看到您的完整解决方案,并惊叹于数学的辉煌:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.