Generando expresiones matemáticas aleatorias

16

Tengo esta idea dando vueltas en mi cabeza, para generar y evaluar expresiones matemáticas aleatorias. Entonces, decidí darle una oportunidad y elaborar un algoritmo, antes de codificarlo para probarlo.

Ejemplo:

Aquí hay algunas expresiones de ejemplo que quiero generar al azar:

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]

Las fáciles y medianas son bastante sencillas. Aleatorios intseparados por operadores aleatorios, nada loco aquí. Pero estoy teniendo algunos problemas para empezar con algo que podría crear uno de los duros y difíciles ejemplos. Ni siquiera estoy seguro de que un solo algoritmo pueda darme los dos últimos.

Lo que estoy considerando:

No puedo decir que probé esas ideas, porque realmente no quería perder mucho tiempo yendo en una dirección que no tenía ninguna posibilidad de trabajar en primer lugar. Pero aún así, pensé en un par de soluciones:

  • Usando árboles
  • Usando expresiones regulares
  • Usando un loco "for-type" loop (seguramente el peor)

Lo que estoy buscando:

Me gustaría saber qué camino crees que es el mejor, entre las soluciones que consideré y tus propias ideas.

Si ve una buena manera de comenzar, le agradecería una pista en la dirección correcta, por ejemplo, con el comienzo del algoritmo, o una estructura general del mismo.

También tenga en cuenta que tendré que evaluar esas expresiones. Esto se puede hacer después de que se genera la expresión o durante su creación. Si toma eso en consideración en su respuesta, eso es genial.

No busco nada relacionado con el lenguaje, pero para el registro, estoy pensando en implementarlo en Objective-C, ya que ese es el lenguaje con el que estoy trabajando más recientemente.

Esos ejemplos no incluyeron el :operador, ya que solo quiero manipular ints, y este operador agrega muchas verificaciones. Si su respuesta le da una solución para manejar este, eso es genial.

Si mi pregunta necesita alguna aclaración, por favor pregunte en los comentarios. Gracias por tu ayuda.

rdurand
fuente
2
hmmm, agrega una función de acondicionamiento físico y parece que te diriges hacia la programación genética .
Philip

Respuestas:

19

Aquí hay una interpretación teórica de su problema.

Está buscando generar al azar palabras (expresión algebraica) a partir de un lenguaje dado (el conjunto infinito de todas las expresiones algebraicas sintácticamente correctas). Aquí hay una descripción formal de una gramática algebraica simplificada que solo admite sumas y multiplicaciones:

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

Aquí, Ehay una expresión (es decir, una palabra de su idioma) y Ies un símbolo terminal (es decir, no se expande más) que representa un número entero. La definición anterior para Etiene tres reglas de producción . Según esta definición, podemos construir aleatoriamente una aritmética válida de la siguiente manera:

  1. Comience con Eel símbolo único de la palabra de salida.
  2. Elija uniformemente al azar uno de los símbolos no terminales.
  3. Elija uniformemente al azar una de las reglas de producción para ese símbolo y aplíquela.
  4. Repita los pasos 2 - 4 hasta que solo queden símbolos de terminal.
  5. Reemplace todos los símbolos de terminal Ipor enteros aleatorios.

Aquí hay un ejemplo de la aplicación de estos 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))

Supongo que elegiría representar una expresión con una interfaz Expressionimplementada por clases IntExpression, AddExpressiony MultiplyExpression. Los dos últimos tendrían una leftExpressiony rightExpression. TodosExpressionSe requieren subclases para implementar un evaluatemétodo, que funciona de forma recursiva en la estructura de árbol definida por estos objetos e implementa efectivamente el patrón compuesto .

Tenga en cuenta que para la gramática y el algoritmo anteriores, la probabilidad de expandir una expresión Een un símbolo terminal Ies solo p = 1/3, mientras que la probabilidad de expandir una expresión en dos expresiones adicionales es1-p = 2/3 . Por lo tanto, el número esperado de enteros en una fórmula producida por el algoritmo anterior es en realidad infinito. La longitud esperada de una expresión está sujeta a la relación de recurrencia

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

donde l(n)denota la longitud esperada de la expresión aritmética después de la naplicación de las reglas de producción. Por lo tanto, sugiero que asigne una probabilidad bastante alta pa la reglaE -> I modo que termine con una expresión bastante pequeña con alta probabilidad.

EDITAR : si le preocupa que la gramática anterior produzca demasiados paréntesis, mire la respuesta de Sebastián Negraszus , cuya gramática evita este problema con mucha elegancia.

blubb
fuente
Whoa .. Eso es genial, me gusta mucho, gracias! Todavía tengo que analizar un poco más todas las soluciones sugeridas para tomar la decisión correcta. Gracias de nuevo, gran respuesta.
rdurand
Gracias por tu edición, eso es algo en lo que no pensé. ¿Crees que limitar el número de veces que pasas por los pasos 2-4 podría funcionar? Digamos, después de 4 (o lo que sea) iteraciones de los pasos 2-4, ¿solo permite la regla E-> I ?
rdurand
1
@rdurand: Sí, por supuesto. Digamos que después de las miteraciones de 2-4, 'ignoras' las reglas de producción recursivas. Esto conducirá a una expresión del tamaño esperado l(m). Sin embargo, tenga en cuenta que esto (teóricamente) no es necesario, ya que la probabilidad de generar una expresión infinita es cero, a pesar de que el tamaño esperado es infinito. Sin embargo, su enfoque es favorable ya que en la práctica, la memoria no solo es finita, sino también pequeña :)
blubb
Con su solución, no veo una manera de resolver la expresión mientras la construyo. Hay alguna ? Todavía puedo resolverlo después, pero prefiero no hacerlo.
rdurand
Si lo desea, ¿por qué no comenzar con un número aleatorio como la expresión base y descomponerlo aleatoriamente (reescribirlo) en operaciones, de la manera descrita blubb? Entonces, no solo tendría la solución para toda la expresión, sino que también obtendría fácilmente soluciones para cada una de las ramas del árbol de expresión.
mikołak
7

en primer lugar, en realidad generaría la expresión en notación postfix , puede convertir fácilmente a infijo o evaluar después de generar su expresión aleatoria, pero hacerlo en postfix significa que no necesita preocuparse por paréntesis o precedencia.

También mantendría un total acumulado de la cantidad de términos disponibles para el siguiente operador en su expresión (suponiendo que desee evitar generar expresiones que estén mal formadas), es decir, algo así:

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, este es un pseudocódigo, por lo que no se prueba / puede contener errores y probablemente no usaría una cadena sino una pila de algún tipo de unión discriminada

jk.
fuente
Actualmente, esto supone que todos los operadores son binarios, pero es bastante fácil de extender con operadores de diferente arity
jk.
Muchas gracias. No pensé en RPN, esa es una buena idea. Buscaré todas las respuestas antes de aceptar una, pero creo que podría hacer que esto funcione.
rdurand
+1 para la corrección posterior. Puede eliminar la necesidad de usar algo más que una pila, lo que creo que es más simple que construir un árbol.
Neil
2
@rdurand Parte de la ventaja de la corrección posterior significa que no tiene que preocuparse por la precedencia (eso ya se ha tenido en cuenta antes de agregarlo a la pila posterior a la corrección). Después de lo cual, simplemente despliega todos los operandos que encuentre hasta que aparezca el primer operador que encuentre en la pila y luego empuje a la pila el resultado y continúe de esta manera hasta que saque el último valor de la pila.
Neil
1
@rdurand La expresión 2+4*6-3+7se convierte en una pila posterior a la corrección + 7 - 3 + 2 * 4 6(la parte superior de la pila es la derecha) Presiona 4 y 6 y aplica el operador *, luego presiona 24 nuevamente. Luego aparece 24 y 2 y aplica el operador +, luego presiona 26 nuevamente. Continúa de esta manera y encontrarás que obtendrás la respuesta correcta. Tenga en cuenta que * 4 6son los primeros términos en la pila. Eso significa que se realiza primero porque ya ha determinado la precedencia sin requerir paréntesis.
Neil
4

La respuesta de blubb es un buen comienzo, pero su gramática formal crea demasiadas paréntesis.

Aquí está mi opinión al respecto:

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

Ees una expresión, Iun entero y Mes una expresión que es un argumento para una operación de multiplicación.

Sebastian Negraszus
fuente
1
Bonita extensión, ¡esta ciertamente se ve menos desordenada!
blubb
Como comenté sobre la respuesta de blubb, conservaré algunos paréntesis no deseados. Tal vez haga que el azar sea "menos aleatorio";) ¡gracias por el complemento!
rdurand
3

Los paréntesis en la expresión "dura" representan el orden de evaluación. En lugar de tratar de generar la forma visualizada directamente, simplemente cree una lista de operadores en orden aleatorio y obtenga la forma visualizada de la expresión a partir de eso.

Números: 1 3 3 9 7 2

Operadores: + * / + *

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

Derivar la forma de visualización es un algoritmo recursivo relativamente simple.

Actualización: aquí hay un algoritmo en Perl para generar el formulario de visualización. Porque +y *son distributivos, aleatoriza el orden de los términos para esos operadores. Eso ayuda a evitar que todos los paréntesis se acumulen en un 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";

fuente
Este algoritmo parece generar siempre árboles desequilibrados: la rama izquierda es profunda, mientras que la derecha es solo un número. Habría demasiados parans iniciales al comienzo de cada expresión, y el orden de las operaciones siempre se deja de derecha a izquierda.
scriptin
Gracias por tu respuesta, Dan, ayuda. Pero @scriptin, ¿no entiendo lo que no te gusta en esta respuesta? ¿Podrías explicar un poco?
rdurand
@scriptin, que se puede solucionar con una aleatorización simple del orden de visualización. Ver la actualización.
@rdurand @ dan1111 He probado el script. El problema del gran subárbol izquierdo está solucionado, pero el árbol generado todavía está muy desequilibrado. Esta imagen muestra lo que quiero decir. Esto no puede ser considerado un problema, pero lleva a la situación en la que subexpresiones gusta (A + B) * (C + D)son nunca se presentan en las expresiones generadas, y también hay una gran cantidad de parens anidados.
scriptin
3
@scriptin, después de pensar en esto, estoy de acuerdo en que es un problema.
2

Para expandir el enfoque de árbol, digamos que cada nodo es una hoja o una expresión binaria:

Node := Leaf | Node Operator Node

Tenga en cuenta que una hoja es solo un entero generado aleatoriamente aquí.

Ahora, podemos generar aleatoriamente un árbol. Decidir la probabilidad de que cada nodo sea una hoja nos permite controlar la profundidad esperada, aunque es posible que también desee una profundidad 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())

Luego, la regla más simple para imprimir el árbol es envolver ()cada expresión que no sea de hoja y evitar preocuparse por la precedencia del operador.


Por ejemplo, si escribo entre paréntesis su última expresión de muestra:

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

Puedes leer el árbol que lo generaría:

                    SUB
                  /      \
               MUL        MUL
             /     6     /   2
          ADD          ADD
         /   3        3   7
       SUB
      8   1
Inútil
fuente
1

Yo usaría árboles. Pueden darle un gran control sobre la generación de las expresiones. Por ejemplo, puede limitar la profundidad por rama y el ancho de cada nivel por separado. La generación basada en árbol también da la respuesta ya durante la generación, lo cual es útil si desea asegurarse de que también el resultado (y los resultados secundarios) sean lo suficientemente difíciles y / o no demasiado difíciles de resolver. Especialmente si agrega un operador de división en algún momento, puede generar expresiones que evalúen números enteros.

msell
fuente
Gracias por tu respuesta. Tenía la misma idea sobre los árboles, poder evaluar / verificar subexpresiones. ¿Tal vez podría dar un poco más de detalles sobre su solución? ¿Cómo construirías un árbol así (no cómo realmente, sino cuál sería la estructura general)?
rdurand
1

Aquí hay una versión ligeramente diferente de la excelente respuesta de Blubb:

Lo que está tratando de construir aquí es esencialmente un analizador que opera en reversa. Lo que su problema y un analizador tienen en común es una gramática libre de contexto , esta en forma de 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> ')'

Los analizadores comienzan con un flujo de terminales (tokens literales como 5o *) e intentan ensamblarlos en no terminales (elementos compuestos de terminales y otros no terminales, como numbero op). Su problema comienza con no terminales y funciona a la inversa, seleccionando cualquier cosa entre los símbolos "o" (tubería) al azar cuando se encuentra uno y repitiendo el proceso de forma recursiva hasta llegar a un terminal.

Un par de las otras respuestas han sugerido que este es un problema de árbol, que es para una cierta clase estrecha de casos en los que no hay no terminales que se refieren directa o indirectamente a través de otro no terminal. Como las gramáticas lo permiten, este problema es realmente un gráfico dirigido . (Las referencias indirectas a través de otros no terminales también cuentan para esto).

Hubo un programa llamado Spew publicado en Usenet a fines de la década de 1980 que fue diseñado originalmente para generar titulares sensacionalistas al azar y también es un gran vehículo para experimentar con estas "gramáticas inversas". Funciona leyendo una plantilla que dirige la producción de una secuencia aleatoria de terminales. Más allá de su valor de diversión (titulares, canciones country, galimatías pronunciables en inglés), he escrito numerosas plantillas que son útiles para generar datos de prueba que van desde texto sin formato a XML hasta C. sintácticamente correcta pero no compilable a pesar de tener 26 años. y escrito en K&R C y con un formato de plantilla feo, se compila muy bien y funciona como se anuncia. Creé una plantilla que resuelve su problema y publiqué en pastebin ya que agregar tanto texto aquí no parece apropiado.

Blrfl
fuente