Generazione di espressioni matematiche casuali


16

Ho questa idea che corre nella mia testa, per generare e valutare espressioni matematiche casuali. Così, ho deciso di provarlo e di elaborare un algoritmo, prima di codificarlo per testarlo.

Esempio:

Ecco alcune espressioni di esempio che voglio generare in modo casuale:

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]

Le semplici e medie quelli sono abbastanza straight-forward. I casuali sono intseparati da operatori casuali, niente di folle qui. Ma sto avendo qualche problema per iniziare con qualcosa che potrebbe creare uno dei duri e più dure esempi. Non sono nemmeno sicuro che un singolo algoritmo possa darmi gli ultimi due.

Quello che sto considerando:

Non posso dire di aver provato queste idee, perché non volevo davvero perdere molto tempo andando in una direzione che non aveva possibilità di lavorare in primo luogo. Tuttavia, ho pensato a un paio di soluzioni:

  • Usando gli alberi
  • Usando espressioni regolari
  • Utilizzando un folle loop "for-type" (sicuramente il peggiore)

Cosa sto cercando:

Mi piacerebbe sapere in che modo ritieni sia il migliore da percorrere, tra le soluzioni che ho preso in considerazione e le tue idee.

Se vedi un buon modo per iniziare, apprezzerei un vantaggio nella giusta direzione, ad esempio con l'inizio dell'algoritmo o una sua struttura generale.

Nota anche che dovrò valutare quelle espressioni. Questo può essere fatto dopo la generazione dell'espressione o durante la sua creazione. Se lo prendi in considerazione nella tua risposta, è fantastico.

Non sto cercando nulla legato al linguaggio, ma per la cronaca, sto pensando di implementarlo in Objective-C, poiché è il linguaggio con cui sto lavorando di più di recente.

Tali esempi non includevano l' :operatore, poiché desidero solo manipolare ints, e questo operatore aggiunge molte verifiche. Se la tua risposta fornisce una soluzione per gestirla, va benissimo.

Se la mia domanda necessita di chiarimenti, si prega di chiedere nei commenti. Grazie per l'aiuto.


2
hmmm, aggiungi una funzione di fitness e sembra che tu sia diretto verso la programmazione genetica .
Filippo,

Risposte:


19

Ecco un'interpretazione teorica del tuo problema.

Stai cercando di generare in modo casuale parole (espressione algebrica) da una determinata lingua (l'insieme infinito di tutte le espressioni algebriche sintatticamente corrette). Ecco una descrizione formale di una grammatica algebrica semplificata che supporta solo addizioni e moltiplicazioni:

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

Qui Ec'è un'espressione (cioè una parola della tua lingua) ed Iè un simbolo terminale (cioè non è più espanso) che rappresenta un numero intero. La definizione sopra per Eha tre regole di produzione . Sulla base di questa definizione, possiamo costruire casualmente un'aritmetica valida come segue:

  1. Inizia con Eil singolo simbolo della parola di output.
  2. Scegli uniformemente a caso uno dei simboli non terminali.
  3. Scegli uniformemente a caso una delle regole di produzione per quel simbolo e applicala.
  4. Ripetere i passaggi da 2 a 4 fino a quando rimangono solo i simboli dei terminali.
  5. Sostituisci tutti i simboli dei terminali Icon numeri interi casuali.

Ecco un esempio dell'applicazione di questo algoritmo:

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

Presumo si sceglie di rappresentare un'espressione con un'interfaccia Expressionche viene realizzato dalle classi IntExpression, AddExpressione MultiplyExpression. Gli ultimi due avrebbero quindi un leftExpressione rightExpression. Tutte le Expressionsottoclassi sono necessarie per implementare un evaluatemetodo, che lavora in modo ricorsivo sulla struttura ad albero definita da questi oggetti e implementa efficacemente il modello composito .

Si noti che per la grammatica e l'algoritmo di cui sopra, la probabilità di espandere un'espressione Ein un simbolo terminale Iè solo p = 1/3, mentre la probabilità di espandere un'espressione in due ulteriori espressioni è 1-p = 2/3. Pertanto, il numero previsto di numeri interi in una formula prodotta dall'algoritmo sopra è in realtà infinito. La lunghezza prevista di un'espressione è soggetta alla relazione di ricorrenza

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

dove l(n)indica la lunghezza attesa dell'espressione aritmetica dopo l' napplicazione delle regole di produzione. Pertanto suggerisco di assegnare una probabilità piuttosto elevata palla regola in E -> Imodo da finire con un'espressione abbastanza piccola con una probabilità elevata.

EDIT : Se sei preoccupato che la grammatica sopra produca troppe parentesi, guarda la risposta di Sebastian Negraszus , la cui grammatica evita questo problema in modo molto elegante.


Whoa .. Fantastico, mi piace molto, grazie! Devo ancora esaminare un po 'di più tutte le soluzioni suggerite per fare la scelta giusta. Grazie ancora, ottima risposta.
rdurand,

Grazie per la tua modifica, è qualcosa a cui non ho pensato. Pensi che limitare il numero di volte che passi dai passaggi 2-4 potrebbe funzionare? Ad esempio, dopo 4 (o qualunque altra) iterazione dei passaggi 2-4, consenti solo la regola E-> I ?
rdurand,

1
@rdurand: Sì, certo. Dì dopo le miterazioni di 2-4, "ignori" le regole di produzione ricorsive. Ciò porterà a un'espressione della dimensione prevista l(m). Si noti tuttavia che ciò non è (teoricamente) necessario, poiché la probabilità di generare un'espressione infinita è zero, anche se la dimensione prevista è infinita. Tuttavia, il tuo approccio è favorevole poiché in pratica, la memoria non è solo limitata, ma anche piccola :)
blubb

Con la tua soluzione, non vedo un modo per risolvere l'espressione mentre la costruisco. C'è qualche ? Posso ancora risolverlo in seguito, ma preferirei di no.
rdurand,

Se lo desideri, perché non iniziare con un numero casuale come espressione di base e scomporlo in modo casuale (riscriverlo) in operazioni, nel modo descritto da Blubb? Quindi, non solo avresti la soluzione per l'intera espressione, ma otterresti anche facilmente delle subsoluzioni per ciascuno dei rami dell'albero delle espressioni.
mikołak,

7

prima di tutto genererei effettivamente l'espressione in notazione postfix , puoi facilmente convertirli in infissi o valutare dopo aver generato la tua espressione casuale, ma farlo in postfix significa che non devi preoccuparti di parentesi o precedenza.

Terrei anche un totale parziale del numero di termini disponibili per il prossimo operatore nella tua espressione (supponendo che tu voglia evitare di generare espressioni che sono malformate) cioè qualcosa del genere:

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

ovviamente questo è pseudo codice quindi non è testato / potrebbe contenere errori e probabilmente non useresti una stringa ma uno stack di un tipo di unione discriminato


questo attualmente presuppone che tutti gli operatori siano binari, ma è abbastanza facile estenderli con operatori di diversa arity
jk.

Molte grazie. Non ho pensato a RPN, è una buona idea. Esaminerò tutte le risposte prima di accettarne una, ma penso che potrei farlo funzionare.
rdurand,

+1 per post-correzione. Puoi eliminare la necessità di usare qualcosa di più di una pila, che penso sia più semplice della costruzione di un albero.
Neil,

2
@rdurand Parte del vantaggio di post-fix significa che non devi preoccuparti della precedenza (che è già stata presa in considerazione prima di aggiungerla allo stack post-fix). Dopodiché, fai semplicemente saltare tutti gli operandi che trovi fino a quando non fai scoppiare il primo operatore che trovi nello stack e poi spingi nello stack il risultato e continui in questo modo fino a quando non elimini l'ultimo valore dallo stack.
Neil,

1
@rdurand L'espressione 2+4*6-3+7viene convertita in stack post-fix + 7 - 3 + 2 * 4 6(la parte superiore dello stack è quella più a destra). Si preme 4 e 6 e si applica l'operatore *, quindi si reinserisce 24. Quindi si aprono 24 e 2 e si applica l'operatore +, quindi si reinserisce 26. Continui in questo modo e scoprirai che otterrai la risposta giusta. Si noti che * 4 6sono i primi termini in pila. Ciò significa che viene eseguito per primo perché hai già determinato la precedenza senza richiedere parentesi.
Neil,

4

La risposta di Blubb è un buon inizio, ma la sua grammatica formale crea troppe paratie.

Ecco la mia opinione su di esso:

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

Eè un'espressione, Iun numero intero ed Mè un'espressione che è un argomento per un'operazione di moltiplicazione.


1
Bella estensione, questa sembra sicuramente meno ingombra!
Blubb,

Mentre commentavo la risposta di Blubb, terrò delle parentesi indesiderate. Forse rendere il casuale "meno casuale";) grazie per il componente aggiuntivo!
rdurand,

3

Le parentesi nell'espressione "difficile" rappresentano l'ordine di valutazione. Invece di provare a generare direttamente il modulo visualizzato, basta creare un elenco di operatori in ordine casuale e ricavarne da esso il formato di visualizzazione dell'espressione.

Numeri: 1 3 3 9 7 2

operatori: + * / + *

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

Derivare il modulo di visualizzazione è un algoritmo ricorsivo relativamente semplice.

Aggiornamento: ecco un algoritmo in Perl per generare il modulo di visualizzazione. Perché +e *sono distributivi, randomizza l'ordine dei termini per quegli operatori. Ciò aiuta a evitare che le parentesi si accumulino da un lato.

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

Questo algoritmo sembra generare sempre alberi sbilanciati: il ramo sinistro è profondo, mentre quello destro è solo un singolo numero. Ci sarebbero troppi paran di apertura all'inizio di ogni espressione e l'ordine delle operazioni è sempre da sinistra a destra.
scriptin,

Grazie per la tua risposta, dan, aiuta. Ma @scriptin, non capisco cosa non ti piace in questa risposta? Puoi spiegarci un po '?
rdurand,

@scriptin, che può essere risolto con una semplice randomizzazione dell'ordine di visualizzazione. Vedi l'aggiornamento

@rdurand @ dan1111 Ho provato lo script. Il problema della sottostruttura grande sinistra è stato risolto, ma l'albero generato è ancora molto squilibrato. Questa immagine mostra cosa intendo. Questo non può essere considerato un problema, ma porta a situazioni in cui le sottoespressioni come non(A + B) * (C + D) sono mai presentate in espressioni generate e ci sono anche molte parentesi nidificate.
scriptin,

3
@scriptin, dopo averci pensato, sono d'accordo che questo è un problema.

2

Per espandere l'approccio ad albero, supponiamo che ogni nodo sia una foglia o un'espressione binaria:

Node := Leaf | Node Operator Node

Nota che qui una foglia è solo un numero intero generato casualmente.

Ora possiamo generare casualmente un albero. Decidere la probabilità che ciascun nodo sia una foglia ci consente di controllare la profondità prevista, anche se potresti desiderare anche una profondità massima assoluta:

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

Quindi, la regola più semplice per stampare l'albero è di avvolgere ()ogni espressione non foglia ed evitare di preoccuparsi della precedenza dell'operatore.


Ad esempio, se parentesi la tua ultima espressione di esempio:

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

puoi leggere l'albero che lo genererebbe:

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

1

Vorrei usare gli alberi. Possono darti un grande controllo sulla generazione delle espressioni. Ad esempio è possibile limitare la profondità per ramo e la larghezza di ciascun livello separatamente. La generazione basata su alberi fornisce anche la risposta già durante la generazione, il che è utile se si desidera garantire che anche il risultato (e i risultati secondari) siano abbastanza difficili e / o non troppo difficili da risolvere. Soprattutto se aggiungi un operatore di divisione ad un certo punto, puoi generare espressioni che valutano numeri interi.


Grazie per la tua risposta. Ho avuto la stessa idea sugli alberi, potendo valutare / controllare le sottoespressioni. Forse potresti dare qualche dettaglio in più sulla tua soluzione? Come costruiresti un tale albero (non quanto realmente, ma quale sarebbe la struttura generale)?
rdurand,

1

Ecco una versione leggermente diversa dell'ottima risposta di Blubb:

Quello che stai cercando di creare qui è essenzialmente un parser che funziona al contrario. Ciò che hanno in comune il tuo problema e un parser è una grammatica senza contesto , questa in formato 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> ')'

I parser iniziano con un flusso di terminali (token letterali come 5o *) e provano a assemblarli in non terminali (cose composte da terminali e altri non terminali, come numbero op). Il tuo problema inizia con i non-terminali e funziona al contrario, scegliendo casualmente qualsiasi cosa tra i simboli "o" (pipe) quando si incontra uno e ripetendo ricorsivamente il processo fino a raggiungere un terminale.

Un paio di altre risposte hanno suggerito che questo è un problema ad albero, che è per una certa classe ristretta di casi in cui non ci sono non-terminali che si riferiscono direttamente o indirettamente attraverso un altro non-terminale. Poiché le grammatiche lo consentono, questo problema è in realtà un grafico diretto . (Anche i riferimenti indiretti attraverso un altro non-terminale contano ai fini di questo.)

C'era un programma chiamato Spew pubblicato su Usenet alla fine degli anni '80, che originariamente era stato progettato per generare titoli di tabloid casuali e sembra essere anche un ottimo veicolo per sperimentare queste "grammatiche inverse". Funziona leggendo un modello che dirige la produzione di un flusso casuale di terminali. Oltre al suo valore di divertimento (titoli, canzoni country, pronunciabile inglese senza senso), ho scritto numerosi modelli utili per generare dati di test che vanno dal semplice testo a XML a sintatticamente corretti ma non compilabili C. Nonostante abbia 26 anni e scritto in K&R C e con un brutto formato modello, si compila bene e funziona come pubblicizzato. Ho preparato un modello che risolve il tuo problema e l' ho pubblicato su pastebin poiché l'aggiunta di molto testo qui non sembra appropriata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.