简化数学表达式的策略

时间:2011-09-24 16:23:34

标签: algorithm math simplify

我有一个结构良好的树,代表一个数学表达式。例如,给定字符串:"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 th 根的42 th 幂)是42

我意识到这个问题非常广泛,但是我已经打了一段时间反对这一点并没有提出足够令人满意的事情。

6 个答案:

答案 0 :(得分:83)

您可能想要实施term rewriting system。关于基础数学,请查看WikiPedia

术语重写模块的结构

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

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

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

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

  • 最后,实现一个类CRuleSet,它只是一组CRule对象。只要不再应用规则,主要方法reduce(CExpression)就会应用规则集,然后返回简化表达式。

  • 此外,您还需要一个类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's DDMathParser是语法分析数学表达式的完美候选者。

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

更年轻,enourmous set of ready to use syntax-filesANTLR是基于Java的现代解析器生成器。除了纯粹的命令行使用,ANTLRWorks提供 GUI前端来构建和调试基于ANTLR的解析器。 ANTLR为various host language生成语法,如JAVA, C, Python, PHP or C#。 ActionScript运行时当前为broken

如果您想从下至上学习如何解析表达式(或一般语言),我建议free book's text from Niklaus Wirth(或{{3 Pascal和Modula-2的着名发明者。

答案 1 :(得分:11)

此任务可能变得非常复杂(除了最简单的转换)。基本上这就是algebra software一直在做的事情。

您可以找到一个可读的介绍如何完成此操作(基于规则的评估),例如: Mathematica

答案 2 :(得分:8)

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

我知道有些系统会构建首先减少常量的树,然后将树放入规范化的形式,然后使用large database of proven/known formulas将问题转换为其他形式。

答案 3 :(得分:2)

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

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

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

乘法可以表示为加法,因此一个规则可能是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。可能有一个+ b-a。

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

答案 4 :(得分:0)

你实际上通常不能这样做,因为虽然它们在数学上是相同的,但在计算机算术中可能不一样。例如,-MAX_INT未定义,因此 - %a = / =%a。同样对于花车,你必须适当地处理NaN和Inf。

答案 5 :(得分:0)

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

虽然这是原始的,但我认为这是问题的第一步,并且会解决你在问题中提到的很多案例。

我期待看到你的完整解决方案并敬畏于数学上的辉煌:)