Gerando expressão matemática aleatória

16

Eu tenho essa idéia circulando na minha cabeça, para gerar e avaliar expressões matemáticas aleatórias. Então, decidi tentar e elaborar um algoritmo, antes de codificá-lo para testá-lo.

Exemplo:

Aqui estão alguns exemplos de expressões que eu quero gerar aleatoriamente:

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]

Os fáceis e médios são bem diretos. Random inté separado por operadores aleatórios, nada de louco aqui. Mas eu estou tendo alguns problemas para começar com algo que poderia criar uma das duras e mais duras exemplos. Não tenho certeza de que um único algoritmo possa me dar os dois últimos.

O que estou considerando:

Não posso dizer que tentei essas idéias, porque realmente não queria perder muito tempo indo em uma direção que não tinha chance de trabalhar em primeiro lugar. Mas ainda assim, pensei em algumas soluções:

  • Usando árvores
  • Usando expressões regulares
  • Usando um loop "for-type" louco (certamente o pior)

O que estou olhando:

Gostaria de saber qual o melhor caminho a seguir, entre as soluções que considerei e suas próprias idéias.

Se você vir uma boa maneira de começar, eu apreciaria uma vantagem na direção certa, por exemplo, com o início do algoritmo ou com uma estrutura geral dele.

Observe também que terei que avaliar essas expressões. Isso pode ser feito após a expressão ser gerada ou durante sua criação. Se você levar isso em consideração na sua resposta, isso é ótimo.

Não estou procurando nada relacionado à linguagem, mas, para o registro, estou pensando em implementá-la no Objective-C, pois é a linguagem com a qual mais estou trabalhando recentemente.

Esses exemplos não incluíram o :operador, já que eu quero apenas manipular ints, e esse operador adiciona muitas verificações. Se sua resposta fornecer uma solução para lidar com essa, isso é ótimo.

Se minha pergunta precisar de algum esclarecimento, pergunte nos comentários. Obrigado pela ajuda.

rdurand
fonte
2
hum, adicione uma função de condicionamento físico e parece que você está direcionado para a programação genética .
23413 Philip

Respostas:

19

Aqui está uma interpretação teórica do seu problema.

Você está procurando gerar aleatoriamente palavras (expressão algébrica) a partir de um determinado idioma (o conjunto infinito de todas as expressões algébricas sintaticamente corretas). Aqui está uma descrição formal de uma gramática algébrica simplificada que suporta apenas adição e multiplicação:

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

Aqui, Eestá uma expressão (ou seja, uma palavra do seu idioma) e Ié um símbolo terminal (ou seja, não é expandido mais) representando um número inteiro. A definição acima para Etem três regras de produção . Com base nessa definição, podemos construir aleatoriamente uma aritmética válida da seguinte maneira:

  1. Comece com Eo símbolo único da palavra de saída.
  2. Escolha uniformemente aleatoriamente um dos símbolos não terminais.
  3. Escolha de maneira uniforme, aleatoriamente, uma das regras de produção para esse símbolo e aplique-a.
  4. Repita as etapas 2 - 4 até restarem apenas os símbolos do terminal.
  5. Substitua todos os símbolos do terminal Ipor números inteiros aleatórios.

Aqui está um exemplo da aplicação desses algoritmos:

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))

Eu suponho que você iria escolher para representar uma expressão com uma interface Expressionque é implementada por classes IntExpression, AddExpressione MultiplyExpression. Os dois últimos teriam um leftExpressione rightExpression. Todas as Expressionsubclasses são necessárias para implementar um evaluatemétodo, que funcione recursivamente na estrutura em árvore definida por esses objetos e efetivamente implemente o padrão composto .

Observe que, para a gramática e algoritmo acima, a probabilidade de expandir uma expressão Eem um símbolo terminal Ié apenas p = 1/3, enquanto a probabilidade de expandir uma expressão em duas expressões adicionais é 1-p = 2/3. Portanto, o número esperado de números inteiros em uma fórmula produzida pelo algoritmo acima é realmente infinito. O comprimento esperado de uma expressão está sujeito à relação de recorrência

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

onde l(n)denota o comprimento esperado da expressão aritmética após a naplicação das regras de produção. Por isso, sugiro que você atribua uma probabilidade bastante alta pà regra, de E -> Imodo que você acabe com uma expressão bastante pequena e com alta probabilidade.

Edição : Se você está preocupado que a gramática acima produz muitos parênteses, observe a resposta de Sebastian Negraszus , cuja gramática evita esse problema com muita elegância.

blubb
fonte
Whoa .. Isso é ótimo, eu gosto muito disso, obrigado! Ainda preciso analisar um pouco mais todas as soluções sugeridas para fazer a escolha certa. Mais uma vez obrigado, ótima resposta.
rdurand
Obrigado pela sua edição, é algo em que não pensei. Você acha que limitar o número de vezes que você passa pelas etapas 2 a 4 pode funcionar? Digamos, após 4 (ou qualquer outra) iterações das etapas 2 a 4, permita apenas a regra E-> I ?
rdurand
1
@rdurand: Sim, claro. Digamos que após as miterações de 2-4, você 'ignore' as regras de produção recursiva. Isso levará a uma expressão do tamanho esperado l(m). Observe, no entanto, que isso não é (teoricamente) necessário, pois a probabilidade de gerar uma expressão infinita é zero, mesmo que o tamanho esperado seja infinito. No entanto, sua abordagem é favorável, pois, na prática, a memória não é apenas finita, mas também pequena :) #
1001
Com a sua solução, não vejo como resolver a expressão enquanto a construo. Existe algum? Ainda posso resolver isso depois, mas prefiro não.
rdurand
Se você deseja isso, por que não começar com um número aleatório como expressão base e decompor aleatoriamente (reescrevê-lo) em operações, da maneira descrita no blubb? Então, você não apenas teria a solução para toda a expressão, mas também obteria facilmente subsoluções para cada um dos ramos da árvore de expressões.
precisa saber é o seguinte
7

Antes de tudo, eu realmente geraria a expressão na notação postfix , você pode facilmente converter em infixo ou avaliar depois de gerar sua expressão aleatória, mas fazê-lo no postfix significa que você não precisa se preocupar com parênteses ou precedência.

Eu também manteria um total em execução do número de termos disponíveis para o próximo operador em sua expressão (supondo que você queira evitar a geração de expressões malformadas), ou seja, algo como isto:

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++;
     }
}

obviamente, esse é um pseudo-código, portanto não é testado / pode conter erros e você provavelmente não usaria uma string, mas uma pilha de alguma união discriminada como o tipo

jk.
fonte
atualmente, assume que todos os operadores são binários, mas é bastante fácil estender-se com operadores de diferentes áreas
jk.
Muito obrigado. Não pensei na RPN, é uma boa ideia. Examinarei todas as respostas antes de aceitar uma, mas acho que poderia fazer isso funcionar.
Rdurand
+1 para pós-correção. Você pode eliminar a necessidade de usar algo além de uma pilha, o que eu acho mais simples do que construir uma árvore.
Neil
2
@rdurand Parte da vantagem do pós-reparo significa que você não precisa se preocupar com precedência (que já foi levada em consideração antes de adicioná-lo à pilha de pós-reparo). Depois disso, você simplesmente popula todos os operandos que encontrar até que apareça o primeiro operador encontrado na pilha e pressiona o resultado na pilha e continua dessa maneira até que apareça o último valor da pilha.
Neil
1
@rdurand A expressão 2+4*6-3+7é convertida na pilha pós-correção + 7 - 3 + 2 * 4 6(a parte superior da pilha está mais à direita). Você pressiona 4 e 6 e aplica o operador *, depois pressiona 24 novamente. Em seguida, você abre 24 e 2 e aplica o operador + e pressiona 26 novamente. Você continua dessa maneira e encontrará a resposta certa. Observe que * 4 6são os primeiros termos da pilha. Isso significa que ele é executado primeiro porque você já determinou a precedência sem precisar de parênteses.
23413 Neil
4

A resposta de Blubb é um bom começo, mas sua gramática formal cria muitas parênteses.

Aqui está a minha opinião:

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

Eé uma expressão, Ium número inteiro e Mé uma expressão que é um argumento para uma operação de multiplicação.

Sebastian Negraszus
fonte
1
Boa extensão, esta certamente parece menos confusa!
blubb
Ao comentar a resposta do blubb, vou manter um parêntese indesejado. Talvez faça o aleatório "menos aleatório";) obrigado pelo complemento!
Rdurand
3

Os parênteses na expressão "rígida" representam a ordem da avaliação. Em vez de tentar gerar o formulário exibido diretamente, basta criar uma lista de operadores em ordem aleatória e derivar o formulário de exibição da expressão.

Números: 1 3 3 9 7 2

Operadores: + * / + *

Resultado: ((1 + 3) * 3 / 9 + 7) * 2

A derivação do formulário de exibição é um algoritmo recursivo relativamente simples.

Atualização: aqui está um algoritmo em Perl para gerar o formulário de exibição. Porque +e *são distributivos, randomiza a ordem dos termos para esses operadores. Isso ajuda a impedir que os parênteses se acumulem de um lado.

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";

fonte
Esse algoritmo parece sempre gerar árvores desequilibradas: o ramo esquerdo é profundo, enquanto o direito é apenas um número. Haveria demasiados parâmetros de abertura no início de cada expressão, e a ordem das operações é sempre da esquerda para a direita.
Script #
Obrigado pela sua resposta, dan, isso ajuda. Mas @scriptin, não entendo o que você não gosta nesta resposta? Você poderia explicar um pouco?
Rdurand
@scriptin, que pode ser corrigido com uma simples randomização da ordem de exibição. Veja a atualização.
@rdurand @ dan1111 Eu tentei o script. A questão da grande subárvore esquerda foi corrigida, mas a árvore gerada ainda é muito desequilibrada. Esta imagem mostra o que quero dizer. Isso pode não ser considerado um problema, mas leva a situações em que subexpressões como nunca(A + B) * (C + D) são apresentadas em expressões geradas e também há muitos parênteses aninhados.
Script #
3
@ Scriptin, depois de pensar sobre isso, eu concordo que isso é um problema.
2

Para expandir a abordagem em árvore, digamos que cada nó seja uma folha ou uma expressão binária:

Node := Leaf | Node Operator Node

Observe que uma folha é apenas um número inteiro gerado aleatoriamente aqui.

Agora, podemos gerar aleatoriamente uma árvore. Decidir a probabilidade de cada nó ser uma folha nos permite controlar a profundidade esperada, embora você também queira uma profundidade máxima absoluta:

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())

Então, a regra mais simples para imprimir a árvore é envolver ()cada expressão que não seja folha e evitar preocupações com a precedência do operador.


Por exemplo, se eu colocar parênteses sua última expressão de amostra:

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

você pode ler a árvore que a geraria:

                    SUB
                  /      \
               MUL        MUL
             /     6     /   2
          ADD          ADD
         /   3        3   7
       SUB
      8   1
Sem utilidade
fonte
1

Eu usaria árvores. Eles podem lhe dar um grande controle sobre a geração das expressões. Por exemplo, você pode limitar a profundidade por ramo e a largura de cada nível separadamente. A geração baseada em árvore também fornece a resposta já durante a geração, o que é útil se você deseja garantir que também o resultado (e sub-resultados) sejam difíceis o suficiente e / ou não muito difíceis de resolver. Especialmente se você adicionar um operador de divisão em algum momento, poderá gerar expressões que serão avaliadas em números inteiros.

msell
fonte
Obrigado pela sua resposta. Eu tive a mesma idéia sobre árvores, sendo capaz de avaliar / verificar subexpressões. Talvez você possa dar um pouquinho mais detalhes sobre sua solução? Como você construiria uma árvore assim (não como realmente, mas qual seria a estrutura geral)?
Rdurand
1

Aqui está uma visão um pouco diferente da excelente resposta de Blubb:

O que você está tentando construir aqui é essencialmente um analisador que opera em sentido inverso. O que seu problema e um analisador têm em comum é uma gramática sem contexto , esta na forma 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> ')'

Os analisadores começam com um fluxo de terminais (tokens literais como 5ou *) e tentam montá-los em não-terminais (itens compostos por terminais e outros não-terminais, como numberouop ). Seu problema começa com os não terminais e funciona ao contrário, escolhendo qualquer coisa entre os símbolos "ou" (canal) aleatoriamente quando um é encontrado e repetindo recursivamente o processo até chegar a um terminal.

Algumas das outras respostas sugeriram que esse é um problema de árvore, que é para uma certa classe restrita de casos em que não existem termos não terminais que se referem direta ou indiretamente a outros não termos. Como as gramáticas permitem isso, esse problema é realmente um gráfico direcionado . (Referências indiretas através de outros não-terminais também contam para isso.)

Havia um programa chamado Spew publicado na Usenet no final dos anos 80, que foi originalmente projetado para gerar manchetes aleatórios dos tablóides e também é um ótimo veículo para experimentar essas "gramáticas reversas". Ele opera lendo um modelo que direciona a produção de um fluxo aleatório de terminais. Além do seu valor de diversão (manchetes, músicas country, rabiscos pronunciados em inglês), escrevi vários modelos que são úteis para gerar dados de teste que variam de texto sem formatação a XML a C. sintaticamente correto, mas não compilável. Apesar de ter 26 anos e escrito em K&R C e com um formato de modelo feio, ele compila muito bem e funciona como anunciado. Eu criei um modelo que resolve o seu problema e publiquei no pastebin já que adicionar muito texto aqui não parece apropriado.

Blrfl
fonte