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 int
separados 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 int
s, 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.
fuente
Respuestas:
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:
Aquí,
E
hay una expresión (es decir, una palabra de su idioma) yI
es un símbolo terminal (es decir, no se expande más) que representa un número entero. La definición anterior paraE
tiene tres reglas de producción . Según esta definición, podemos construir aleatoriamente una aritmética válida de la siguiente manera:E
el símbolo único de la palabra de salida.I
por enteros aleatorios.Aquí hay un ejemplo de la aplicación de estos algoritmos:
Supongo que elegiría representar una expresión con una interfaz
Expression
implementada por clasesIntExpression
,AddExpression
yMultiplyExpression
. Los dos últimos tendrían unaleftExpression
yrightExpression
. TodosExpression
Se requieren subclases para implementar unevaluate
mé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
E
en un símbolo terminalI
es solop = 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 recurrenciadonde
l(n)
denota la longitud esperada de la expresión aritmética después de lan
aplicación de las reglas de producción. Por lo tanto, sugiero que asigne una probabilidad bastante altap
a 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.
fuente
m
iteraciones de 2-4, 'ignoras' las reglas de producción recursivas. Esto conducirá a una expresión del tamaño esperadol(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 :)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í:
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
fuente
2+4*6-3+7
se 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 6
son los primeros términos en la pila. Eso significa que se realiza primero porque ya ha determinado la precedencia sin requerir paréntesis.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
es una expresión,I
un entero yM
es una expresión que es un argumento para una operación de multiplicación.fuente
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.fuente
(A + B) * (C + D)
son nunca se presentan en las expresiones generadas, y también hay una gran cantidad de parens anidados.Para expandir el enfoque de árbol, digamos que cada nodo es una hoja o una expresión binaria:
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:
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:
Puedes leer el árbol que lo generaría:
fuente
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.
fuente
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 :
Los analizadores comienzan con un flujo de terminales (tokens literales como
5
o*
) e intentan ensamblarlos en no terminales (elementos compuestos de terminales y otros no terminales, comonumber
oop
). 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.
fuente