Генерація випадкового математичного вираження


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]

Ці легкі і середні з них є досить прямо вперед. Випадкові ints розділені випадковими операторами, тут немає нічого божевільного. Але у мене виникають проблеми з тим, щоб почати щось із того, що могло б створити один із важких і важких прикладів. Я навіть не впевнений, що жоден алгоритм міг би дати мені останні два.

Що я розглядаю:

Я не можу сказати, що я спробував ці ідеї, тому що мені дуже не хотілося витрачати багато часу, рухаючись у напрямку, в якому не було шансів працювати в першу чергу. Але все ж я подумав про пару рішень:

  • Використання дерев
  • Використання регулярних виразів
  • Використання шаленого циклу "для типу" (безумовно, найгірше)

Що я шукаю:

Мені хотілося б знати, яким шляхом ви вважаєте, що найкраще пройти між рішеннями, які я вважав, та вашими власними ідеями.

Якщо ви бачите хороший спосіб почати, я буду вдячний за лідер у правильному напрямку, наприклад, з початком роботи алгоритму або загальної структури його.

Також зауважте, що мені доведеться оцінити ці вирази. Це можна зробити або після генерування виразу, або під час його створення. Якщо ви врахуєте це у своїй відповіді, це чудово.

Я не шукаю нічого, що стосується мови, але для запису, я думаю про те, щоб реалізувати це в Objective-C, оскільки це мова, з якою я найбільше працюю останнім часом.

Ці приклади не включали :оператора, оскільки я хочу лише маніпулювати ints, і цей оператор додає багато перевірок. Якщо у вашій відповіді дано рішення, яке стосується цього, це чудово.

Якщо моє запитання потребує уточнення, будь ласка, запитайте у коментарях. Спасибі за вашу допомогу.


2
хммм, додайте функцію фітнесу, і схоже, що ви направляєтесь до генетичного програмування .
Філіп

Відповіді:


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який реалізується класами IntExpression, AddExpressionта MultiplyExpression. Останні два тоді мали б leftExpressionі rightExpression. Усі 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ви закінчилися досить невеликим виразом з високою ймовірністю.

EDIT : Якщо ви переживаєте, що вищевказана граматика дає занадто багато круглих дужок, подивіться на відповідь Себастьяна Неграша , граматика якого дуже елегантно уникає цієї проблеми.


Ого, це чудово, мені це дуже подобається, дякую! Мені все ж доведеться трохи більше розглянути всі рішення, запропоновані, щоб зробити правильний вибір. Ще раз дякую, чудова відповідь.
rdurand

Дякую за вашу редакцію, це те, про що я не думав. Як ви думаєте, обмеження кількості разів, які ви пройдете через кроки 2-4, може спрацювати? Скажімо, після 4 (чи будь-яких) ітерацій кроків 2-4 дозволяється тільки правило E-> I ?
rdurand

1
@rdurand: Так, звичайно. Скажіть, після mітерацій 2-4, ви "ігноруєте" рекурсивні правила виробництва. Це призведе до вираження очікуваного розміру l(m). Однак зауважимо, що це (теоретично) не є необхідним, оскільки ймовірність генерування нескінченного виразу дорівнює нулю, навіть якщо очікуваний розмір нескінченний. Однак ваш підхід сприятливий, оскільки на практиці пам’ять не тільки кінцева, але й невелика :)
похмурість

За допомогою вашого рішення я не бачу способу, як я міг би вирішити вираз, будуючи його. Чи є ? Я все ще можу це вирішити після цього, але краще не робити.
rdurand

Якщо ви цього хочете, чому б не почати з випадкового числа як базового виразу і випадковим чином розкласти його (переписати) на операції, як описано способом, який описаний? Тоді ви не тільки матимете рішення для всього виразу, але й легко отримаєте дозвіл для кожної з гілок дерева виразів.
mikołak

7

Перш за все, я б насправді генерував вираз у позначеннях постфікса , ви можете легко перетворити в інфікс або оцінити після генерування випадкового виразу, але робити це в постфіксі означає, що вам не потрібно турбуватися про дужки чи пріоритет.

Я б також утримував загальну кількість термінів, доступних для наступного оператора у вашому виразі (якщо припустити, що ви хочете уникати генерування виразів, які неправильно формуються), тобто щось подібне:

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

очевидно, що це псевдо-код, тому не перевіряється / може містити помилки, і ви, ймовірно, не використовували б рядок, а стек якогось дискримінованого об'єднання типу типу


На даний момент це передбачає, що всі оператори є двійковими, але їх досить просто розширити з операторами різної сутності
jk.

Дуже дякую. Я не думав про RPN, це гарна ідея. Я вивчу всі відповіді, перш ніж приймати їх, але думаю, що я міг би зробити цю роботу.
rdurand

+1 для після виправлення. Ви можете усунути необхідність використовувати що-небудь більше, ніж стек, що, на мою думку, простіше, ніж будувати дерево.
Ніл

2
@rdurand Частина переваги пост-виправлення означає, що вам не доведеться турбуватися про перевагу (це вже було враховано до того, як додати його до стеку post-fix). Після цього ви просто вискакуєте всі знайдені операнди, поки не виведете першого оператора, який ви знайдете у стеку, а потім натисніть на результат результат і продовжуєте таким чином, поки не виведете останнє значення зі стека.
Ніл

1
@rdurand Вираз 2+4*6-3+7перетворюється в стек після виправлення + 7 - 3 + 2 * 4 6(верхня частина стека - сама права). Ви відштовхуєте 4 і 6 і застосовуєте оператора *, потім натискаєте на 24 назад. Потім ви з'являєтесь 24 і 2 і застосовуєте оператор +, потім натискаєте 26 назад. Ви продовжуєте так, і знайдете правильну відповідь. Зауважте, що * 4 6це перші терміни в стеку. Це означає, що він виконується спочатку, оскільки ви вже визначили пріоритет, не вимагаючи дужок.
Ніл

4

Відповідь Блуба - це хороший початок, але його формальна граматика створює занадто багато парантезів.

Ось мій погляд на це:

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

Eє виразом, Iцілим числом і Mє виразом, який є аргументом для операції множення.


1
Гарне розширення, це, безумовно, виглядає менш захаращеним!
блека

Коли я прокоментував відповідь дури, я збережу небажані дужки. Можливо, зробіть випадковий "менш випадковим";) спасибі за додаток!
rdurand

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, я не розумію, що тобі не подобається у цій відповіді? Не могли б ви пояснити трохи?
rdurand

@scriptin, що можна виправити простою рандомізацією порядку відображення. Дивіться оновлення.

@rdurand @ dan1111 Я спробував сценарій. Проблема великого лівого піддерева вирішена, але генероване дерево все ще дуже незбалансоване. Ця картина показує, що я маю на увазі. Це може не вважатися проблемою, але це призводить до ситуації, коли субекспресії, подібні ніколи(A + B) * (C + D) , не представлені в генерованих виразах, а також є багато вкладених паронів.
сценарій

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

Ось дещо інший погляд на відмінну відповідь Блубба:

Те, що ви намагаєтесь побудувати тут, - це, по суті, аналізатор, який працює в зворотному напрямку. Ваша проблема та аналізатор мають спільне граматика без контексту , ця у формі 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або *) і намагаються зібрати їх у нетермінали (речі, що складаються з терміналів та інших нетерміналів, таких як numberабо op). Ваша проблема починається з нетерміналів і працює в зворотному порядку, вибираючи що-небудь між символами "або" (труба) навмання, коли стикається, і рекурсивно повторює процес до досягнення терміналу.

Кілька інших відповідей припустили, що це проблема дерева, яка є для певного вузького класу випадків, коли немає нетерміналів, які прямо або опосередковано посилаються на інший нетермінал. Оскільки граматики це дозволяють, ця проблема справді є спрямованим графіком . (До цього також враховуються непрямі посилання через інший нетермінал.)

На Usenet була опублікована програма під назвою Spew наприкінці 1980-х, яка спочатку була створена для генерування випадкових заголовків таблоїдів, а також є чудовим засобом для експериментів із цими "зворотними граматиками". Він функціонує, читаючи шаблон, який спрямовує на виробництво випадкового потоку терміналів. Окрім своєї розважальної цінності (заголовки, пісні кантрі, вимовляється англійська гібрид), я написав численні шаблони, які корисні для генерування тестових даних, що варіюються від простого тексту до XML до синтаксично-правильного, але непомітного C. Незважаючи на те, що йому було 26 років і написана на K&R C і має некрасивий формат шаблону, він складається просто чудово і працює як рекламується. Я збив шаблон, який вирішує вашу проблему, і розмістив його на пастібіні оскільки додати, що багато тексту тут не здається підходящим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.