生成随机数学表达式


16

我脑子里到处都是这个想法,以生成和评估随机数学表达式。因此,我决定先尝试一下,然后详细说明一下算法,然后再对其进行编码以对其进行测试。

例:

以下是一些我想随机生成的示例表达式:

4 + 2                           [easy]
3 * 6 - 7 + 2                   [medium]
6 * 2 + (5 - 3) * 3 - 8         [hard]
(3 + 4) + 7 * 2 - 1 - 9         [hard]
5 - 2 + 4 * (8 - (5 + 1)) + 9   [harder]
(8 - 1 + 3) * 6 - ((3 + 7) * 2) [harder]

容易中等的人是相当直接的。随机数int由随机运算符分隔,在这里没有什么疯狂的。但我有一些麻烦开始的东西,可能创造的一个坚硬困难的例子。我什至不确定单个算法能否给我最后两个算法。

我正在考虑的是:

我不能说我尝试过这些想法,因为我真的不想浪费很多时间去朝着根本没有机会工作的方向发展。但是,我仍然想到了一些解决方案:

  • 使用树木
  • 使用正则表达式
  • 使用疯狂的“ for-type”循环(肯定是最糟糕的)

我在寻找什么:

我想知道,在我考虑的解决方案和您自己的想法之间,您认为最好的方法是什么。

如果您看到一个很好的开始方法,那么我将感谢您在正确的方向上取得了领先,例如算法的开始或其总体结构。

另请注意,我将必须评估这些表达式。可以在生成表达式之后或在其创建过程中完成此操作。如果您在回答时考虑到这一点,那就太好了。

我没有在寻找与语言相关的任何东西,但是为了记录,我正在考虑在Objective-C中实现它,因为这是我最近使用最多的语言。

这些示例不包括:运算符,因为我只想操作ints,并且此运算符添加了许多验证。如果您的答案给出了解决此问题的解决方案,那就太好了。

如果我的问题需要澄清,请在评论中提出。谢谢你的帮助。


2
嗯,添加一个适应度函数,您似乎正朝着基因编程的方向前进。
菲利普(Philip)

Answers:


19

这是您问题的理论解释。

您正在寻找从给定语言(所有语法正确的代数表达式的无穷集合)中随机生成单词(代数表达式)的方法。这是仅支持加法和乘法的简化代数语法的正式描述:

E -> I 
E -> (E '+' E)
E -> (E '*' E)

在这里,E是一个表示整数的表达式(即您语言的单词)和I一个终端符号(即不再进一步扩展)。上面的定义E有三个生产规则。基于此定义,我们可以随机构建一个有效的算法,如下所示:

  1. E作为输出单词的单个符号开头。
  2. 随机选择一个非终端符号统一。
  3. 随机选择该符号的生产规则之一,然后应用它。
  4. 重复步骤2-4,直到只剩下端子符号。
  5. 将所有终端符号替换I为随机整数。

这是此算法的应用示例:

E
(E + E)
(E + (E * E))
(E + (I * E))
((E + E) + (I * E))
((I + E) + (I * E))
((I + E) + (I * I))
((I + (E * E)) + (I * I))
((I + (E * I)) + (I * I))
((I + (I * I)) + (I * I))
((2 + (5 * 1)) + (7 * 4))

我想你会选择代表与接口的表达Expression是通过类来实现IntExpressionAddExpressionMultiplyExpression。那么后两个将有leftExpressionrightExpression。所有Expression子类都需要实现一个evaluate方法,该方法在这些对象定义的树结构上递归工作,并有效地实现复合模式

请注意,对于上述语法和算法,将表达式扩展E为终端符号的概率I仅为p = 1/3,而将表达式扩展为两个其他表达式的概率为1-p = 2/3。因此,由上述算法产生的公式中期望的整数个数实际上是无限的。表达式的预期长度取决于递归关系

l(0) = 1
l(n) = p * l(n-1) + (1-p) * (l(n-1) + 1)
     = l(n-1) + (1-p)

其中l(n)表示n应用生产规则后算术表达式的预期长度。因此,我建议您p为规则分配一个较高的概率,E -> I从而最终获得一个具有较高概率的较小表达式。

编辑:如果您担心以上语法会产生过多的括号,请查看塞巴斯蒂安·内格拉苏斯Sebastian Negraszus)的答案,该语法非常优雅地避免了此问题。


哇,太好了,我非常喜欢,谢谢!我仍然需要更多地研究所有建议的解决方案以做出正确的选择。再次感谢,很棒的答案。
rdurand

感谢您的编辑,这是我没想到的。您是否认为限制执行步骤2-4的次数可能有效?假设在步骤2-4进行了4次(或任何其他)迭代之后,仅允许规则E-> I
rdurand 2013年

1
@rdurand:是的,当然。假设m2-4次迭代后,您“忽略”了递归生产规则。这将导致预期大小的表达l(m)。但是请注意,(理论上)这不是必需的,因为即使期望的大小是无限的,生成无限表达式的可能性也为零。但是,您的方法是有利的,因为在实践中,内存不仅是有限的,而且很小:)
blubb 2013年

使用您的解决方案,我看不出在构建表达式时可以解决表达式的方法。有没有 ?之后我仍然可以解决,但我不愿意。
rdurand 2013年

如果需要的话,为什么不以blubb所描述的方式从随机数开始作为基本表达式并将其随机分解(重写)为运算?这样,您不仅可以获得整个表达式的解决方案,而且还可以轻松地获得表达式树的每个分支的子解决方案。
mikołak

7

首先,我实际上会以后缀表示法生成表达式,您可以在生成随机表达式后轻松地将其转换为infix或求值,但是在后缀中进行表示意味着您无需担心括号或优先级。

我还将保持表达式中下一个运算符可用的术语总数(假设您想避免生成格式错误的表达式),例如:

string postfixExpression =""
int termsCount = 0;
while(weWantMoreTerms)
{
    if (termsCount>= 2)
    {
         var next = RandomNumberOrOperator();
         postfixExpression.Append(next);
         if(IsNumber(next)) { termsCount++;}
         else { termsCount--;}
    }
    else
    {
       postfixExpression.Append(RandomNumber);
       termsCount++;
     }
}

显然,这是伪代码,因此未经测试/可能包含错误,您可能不会使用字符串,而是使用一些区分类型的联合(例如type)的堆栈


这个目前假设运营商是二进制的,但它很容易与不同的元数的运营商扩展
JK。

非常感谢。我没有想到RPN,这是个好主意。在接受答案之前,我会仔细研究所有答案,但我认为我可以解决这个问题。
rdurand

+1为后缀。您可以消除使用堆栈以外的任何东西的麻烦,我认为这比构建树更简单。
尼尔

2
@rdurand修复后优点的一部分意味着您不必担心优先级(在将其添加到修复后堆栈之前已经考虑了优先级)。之后,您只需弹出找到的所有操作数,直到弹出在堆栈上找到的第一个运算符,然后将结果压入堆栈,然后以这种方式继续操作,直到从堆栈中弹出最后一个值。
尼尔

1
@rdurand表达式2+4*6-3+7将转换为固定后堆栈+ 7 - 3 + 2 * 4 6(堆栈的最右端)。推4和6并应用运算符*,然后再推24。然后弹出24和2并应用运算符+,然后再按26。您以这种方式继续,您会找到正确的答案。请注意,这* 4 6是堆栈中的第一项。这意味着它在执行第一,因为你已经确定的优先级,而无需括号。
Neil 2013年

4

blubb的答案是一个良好的开端,但是他的形式语法却产生了太多的寄生。

这是我的看法:

E -> I
E -> M '*' M
E -> E '+' E
M -> I
M -> M '*' M
M -> '(' E '+' E ')'

E是一个表达式,I一个整数,并且M是一个作为乘法运算参数的表达式。


1
很好的扩展,这个看起来肯定比较整洁!
blubb 2013年

当我评论blubb的答案时,我会保留一些不需要的括号。也许使随机“较少随机”;)感谢附加组件!
rdurand 2013年

3

“硬”表达式中的括号表示评估顺序。与其尝试直接生成显示形式,不如想出一个随机顺序的运算符列表,然后从中得出表达式的显示形式。

号码: 1 3 3 9 7 2

运营商: + * / + *

结果: ((1 + 3) * 3 / 9 + 7) * 2

导出显示形式是一种相对简单的递归算法。

更新:这是Perl中的一种算法,用于生成显示表单。因为+*是分布式的,所以它将那些运算符的术语顺序随机化。这有助于防止括号在一侧堆积。

use warnings;
use strict;

sub build_expression
{
    my ($num,$op) = @_;

    #Start with the final term.
    my $last_num = pop @$num; 
    my $last_op = pop @$op;

    #Base case: return the number if there is just a number 
    return $last_num unless defined $last_op;

    #Recursively call for the expression minus the final term.
    my $rest = build_expression($num,$op); 

    #Add parentheses if there is a bare + or - and this term is * or /
    $rest = "($rest)" if ($rest =~ /[+-][^)]+$|^[^)]+[+-]/ and $last_op !~ /[+-]/);

    #Return the two components in a random order for + or *.
    return $last_op =~ m|[-/]| || rand(2) >= 1 ? 
        "$rest $last_op $last_num" : "$last_num $last_op $rest";        
}

my @numbers   = qw/1 3 4 3 9 7 2 1 10/;
my @operators = qw|+ + * / + * * +|;

print build_expression([@numbers],[@operators]) , "\n";

该算法似乎总是生成不平衡的树:左分支很深,而右分支只是一个数字。每个表达式的开始处会有太多的开头括号,并且操作顺序始终从左到右。
scriptin

丹,感谢您的回答,它会有所帮助。但是@scriptin,我不明白您在这个答案中不满意的是什么?你能解释一下吗?
rdurand

@scriptin,可以通过简单的显示顺序随机化来解决。查看更新。

@rdurand @ dan1111我已经尝试了脚本。大左子树的问题已解决,但生成的树仍然非常不平衡。这张照片显示了我的意思。这可能不算问题,但是会导致这样的情况:子表达式之类(A + B) * (C + D)的子表达式从未出现在生成的表达式中,并且还有很多嵌套的paren。
scriptin

3
@scriptin,在考虑了这一点之后,我同意这是一个问题。

2

为了扩展树方法,我们假设每个节点都是叶或二进制表达式:

Node := Leaf | Node Operator Node

请注意,此处的叶子只是随机生成的整数。

现在,我们可以随机生成一棵树。确定每个节点成为叶子的可能性使我们可以控制预期的深度,尽管您可能还需要绝对最大深度:

Node random_tree(leaf_prob, max_depth)
    if (max_depth == 0 || random() > leaf_prob)
        return random_leaf()

    LHS = random_tree(leaf_prob, max_depth-1)
    RHS = random_tree(leaf_prob, max_depth-1)
    return Node(LHS, RHS, random_operator())

然后,打印树的最简单规则是将()每个非叶子表达式包装起来,并避免担心运算符的优先级。


例如,如果我在最后一个示例表达式中加上括号:

(8 - 1 + 3) * 6 - ((3 + 7) * 2)
((((8 - 1) + 3) * 6) - ((3 + 7) * 2))

您可以读取将生成它的树:

                    SUB
                  /      \
               MUL        MUL
             /     6     /   2
          ADD          ADD
         /   3        3   7
       SUB
      8   1

1

我会用树。它们可以使您更好地控制表达式的生成。例如,您可以分别限制每个分支的深度和每个级别的宽度。基于树的生成还提供了生成过程中的答案,如果您要确保结果(和子结果)也足够难和/或不太难解决,这将非常有用。特别是如果您在某个时候添加除法运算符,则可以生成计算为整数的表达式。


感谢您的回答。我对树有相同的想法,能够评估/检查子表达式。也许您可以提供有关解决方案的更多细节?你将如何建立这样一个树(不如何真的,但你会总体结构是)?
rdurand

1

这与Blubb的出色答案略有不同:

您要在此处构建的实质上是一个反向运行的解析器。您的问题和解析器的共同点是上下文无关的语法,这是Backus-Naur形式的语法

digit ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
number ::= <digit> | <digit> <number>
op ::= '+' | '-' | '*' | '/'
expr ::= <number> <op> <number> | '(' <expr> ')' | '(' <expr> <op> <expr> ')'

解析器从终端流(诸如5或的文字标记*)开始,然后尝试将它们组合成非终端(由终端和其他非终端组成的事物,例如numberop)。您的问题始于非终结符,反之亦然,遇到一个随机选择在“或”(管道)符号之间的任何东西,然后递归地重复该过程,直到到达终结符为止。

其他几个答案表明这是一个树问题,这是针对某些狭窄的情况,其中没有非终结点直接或通过另一个非终结点间接引用自己。由于语法允许这样做,所以这个问题实际上是有向图。(通过另一个非终结符的间接引用也会计入此。)

1980年代后期在Usenet上发布了一个名为Spew 的程序,该程序最初旨在生成随机的小报标题,并且恰好是尝试使用这些“反语法”的工具。它通过读取一个模板来操作,该模板指导产生随机的终端流。除了具有娱乐性(标题,乡村歌曲,明显的英语胡言乱语)之外,我还编写了许多模板,这些模板可用于生成测试数据,范围从纯文本到XML到语法正确但不可编译的C。尽管已有26岁的历史并以K&R C编写,并且具有丑陋的模板格式,它可以很好地编译,并且可以像宣传的那样工作。我整理了一个模板来解决您的问题,并将其发布在pastebin上 因为在此处添加大量文本似乎不合适。

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.