Tạo biểu thức toán học ngẫu nhiên


16

Tôi có ý tưởng này chạy xung quanh trong đầu, để tạo và đánh giá các biểu thức toán học ngẫu nhiên. Vì vậy, tôi quyết định cho nó một shot và xây dựng một thuật toán, trước khi mã hóa nó để kiểm tra nó.

Thí dụ:

Dưới đây là một số biểu thức ví dụ tôi muốn tạo ngẫu nhiên:

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]

Những người dễ dàngtrung bình là khá đơn giản. intS ngẫu nhiên cách nhau bởi các toán tử ngẫu nhiên, không có gì điên rồ ở đây. Nhưng tôi đang gặp một số rắc rối bắt đầu với cái gì đó có thể tạo ra một trong những khó khănkhó khăn hơn ví dụ. Tôi thậm chí không chắc chắn một thuật toán duy nhất có thể cho tôi hai thuật toán cuối cùng.

Những gì tôi đang xem xét:

Tôi không thể nói rằng tôi đã thử những ý tưởng đó, vì tôi thực sự không muốn lãng phí nhiều thời gian để đi theo hướng không có cơ hội làm việc ngay từ đầu. Nhưng tôi vẫn nghĩ ra một vài giải pháp:

  • Dùng cây
  • Sử dụng biểu thức chính quy
  • Sử dụng một vòng lặp "for-type" điên rồ (chắc chắn là tồi tệ nhất)

Thứ tôi đang tìm kiếm:

Tôi muốn biết con đường nào bạn tin là tốt nhất để đi, giữa các giải pháp tôi đã xem xét và ý tưởng của riêng bạn.

Nếu bạn thấy một cách tốt để bắt đầu, tôi sẽ đánh giá cao một người dẫn đúng hướng, ví dụ như với sự khởi đầu của thuật toán hoặc cấu trúc chung của nó.

Cũng lưu ý rằng tôi sẽ phải đánh giá các biểu thức đó. Điều này có thể được thực hiện sau khi biểu thức được tạo hoặc trong quá trình tạo. Nếu bạn cân nhắc điều đó trong câu trả lời của mình, điều đó thật tuyệt.

Tôi không tìm kiếm bất cứ điều gì liên quan đến ngôn ngữ, nhưng đối với hồ sơ, tôi nghĩ đến việc triển khai nó trong Objective-C, vì đó là ngôn ngữ tôi làm việc gần đây nhất.

Những ví dụ đó không bao gồm :toán tử, vì tôi chỉ muốn thao tác ints và toán tử này thêm nhiều xác minh. Nếu câu trả lời của bạn đưa ra giải pháp xử lý vấn đề này, điều đó thật tuyệt.

Nếu câu hỏi của tôi cần làm rõ, xin vui lòng hỏi trong các ý kiến. Cảm ơn bạn đã giúp đỡ.


2
hmmm, thêm một chức năng thể dục và có vẻ như bạn đang hướng tới lập trình di truyền .
Philip

Câu trả lời:


19

Đây là một giải thích lý thuyết về vấn đề của bạn.

Bạn đang tìm cách tạo ngẫu nhiên các từ (biểu thức đại số) từ một ngôn ngữ nhất định (tập hợp vô hạn của tất cả các biểu thức đại số chính xác về mặt cú pháp). Dưới đây là một mô tả chính thức về một ngữ pháp đại số đơn giản hóa chỉ hỗ trợ phép cộng và phép nhân:

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

Ở đây, Elà một biểu thức (nghĩa là một từ trong ngôn ngữ của bạn) và Ilà một ký hiệu đầu cuối (nghĩa là nó không được mở rộng thêm nữa) đại diện cho một số nguyên. Định nghĩa trên Ecó ba quy tắc sản xuất . Dựa trên định nghĩa này, chúng ta có thể xây dựng ngẫu nhiên một số học hợp lệ như sau:

  1. Bắt đầu bằng Eký hiệu đơn của từ đầu ra.
  2. Chọn thống nhất ngẫu nhiên một trong các ký hiệu không đầu cuối.
  3. Chọn thống nhất một cách ngẫu nhiên một trong các quy tắc sản xuất cho biểu tượng đó và áp dụng nó.
  4. Lặp lại các bước 2 - 4 cho đến khi chỉ còn lại các ký hiệu đầu cuối.
  5. Thay thế tất cả các ký hiệu đầu cuối Ibằng số nguyên ngẫu nhiên.

Đây là một ví dụ về ứng dụng của thuật toán này:

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

Tôi giả sử bạn sẽ chọn để đại diện cho một biểu thức với một giao diện Expressionđược thực hiện bởi các lớp học IntExpression, AddExpressionMultiplyExpression. Hai cái sau sẽ có một leftExpressionrightExpression. Tất cả các Expressionlớp con được yêu cầu để thực hiện một evaluatephương thức, hoạt động đệ quy trên cấu trúc cây được xác định bởi các đối tượng này và thực hiện hiệu quả mô hình tổng hợp .

Lưu ý rằng đối với ngữ pháp và thuật toán ở trên, xác suất mở rộng biểu thức Ethành ký hiệu đầu cuối Ichỉ p = 1/3là xác suất, trong khi xác suất để mở rộng biểu thức thành hai biểu thức tiếp theo là 1-p = 2/3. Do đó, số lượng số nguyên dự kiến ​​trong một công thức được tạo bởi thuật toán trên thực sự là vô hạn. Độ dài dự kiến ​​của một biểu thức tùy thuộc vào mối quan hệ lặp lại

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

trong đó l(n)biểu thị độ dài dự kiến ​​của biểu thức số học sau khi náp dụng quy tắc sản xuất. Do đó, tôi khuyên bạn nên gán một xác suất khá cao pcho quy tắc E -> Isao cho bạn kết thúc bằng một biểu thức khá nhỏ với xác suất cao.

EDIT : Nếu bạn lo lắng rằng ngữ pháp trên tạo ra quá nhiều dấu ngoặc đơn, hãy xem câu trả lời của Sebastian Negraszus , người có ngữ pháp tránh vấn đề này rất thanh lịch.


Whoa .. Thật tuyệt, tôi rất thích điều đó, cảm ơn! Tôi vẫn phải xem xét thêm một chút về tất cả các giải pháp được đề xuất để đưa ra lựa chọn đúng đắn. Cảm ơn một lần nữa, câu trả lời tuyệt vời.
ndurand

Cảm ơn bạn đã chỉnh sửa, đó là điều tôi không nghĩ tới. Bạn có nghĩ rằng việc giới hạn số lần bạn trải qua các bước 2-4 có thể hoạt động không? Nói, sau 4 (hoặc bất cứ điều gì) lặp lại các bước 2-4, chỉ cho phép quy tắc E-> I ?
ndurand

1
@rdurand: Vâng, tất nhiên. Nói sau khi mlặp lại 2-4, bạn 'bỏ qua' các quy tắc sản xuất đệ quy. Điều này sẽ dẫn đến một biểu hiện của kích thước dự kiến l(m). Tuy nhiên, lưu ý rằng điều này (về mặt lý thuyết) là không cần thiết, vì xác suất tạo ra một biểu thức vô hạn là bằng 0, mặc dù kích thước dự kiến ​​là vô hạn. Tuy nhiên, cách tiếp cận của bạn là thuận lợi vì trong thực tế, bộ nhớ không chỉ hữu hạn, mà còn nhỏ :)
blubb

Với giải pháp của bạn, tôi không thấy cách nào tôi có thể giải quyết biểu thức trong khi xây dựng nó. Có cái nào không? Tôi vẫn có thể giải quyết nó sau đó, nhưng tôi không muốn.
ndurand

Nếu bạn muốn điều đó, tại sao không bắt đầu với một số ngẫu nhiên làm biểu thức cơ sở và phân tách ngẫu nhiên (viết lại) thành các hoạt động, theo cách mô tả blubb? Sau đó, bạn không chỉ có giải pháp cho toàn bộ biểu thức, mà bạn còn dễ dàng nhận được các phép con cho mỗi nhánh của cây biểu thức.
mikołak

7

trước hết tôi thực sự tạo ra biểu thức trong ký hiệu postfix , bạn có thể dễ dàng chuyển đổi thành infix hoặc đánh giá sau khi tạo biểu thức ngẫu nhiên, nhưng thực hiện nó trong postfix có nghĩa là bạn không cần phải lo lắng về dấu ngoặc đơn hoặc quyền ưu tiên.

Tôi cũng sẽ giữ tổng số lượng thuật ngữ có sẵn cho toán tử tiếp theo trong biểu thức của bạn (giả sử bạn muốn tránh tạo các biểu thức không đúng định dạng) tức là một số thứ như thế này:

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

rõ ràng đây là mã giả nên không được kiểm tra / có thể chứa lỗi và bạn có thể sẽ không sử dụng một chuỗi mà là một nhóm các loại liên kết bị phân biệt đối xử


điều này hiện tại giả định tất cả các toán tử là nhị phân, nhưng nó khá dễ dàng để mở rộng với các toán tử có mức độ khác nhau
jk.

Cảm ơn rất nhiều. Tôi đã không nghĩ về RPN, đó là một ý tưởng tốt. Tôi sẽ xem xét tất cả các câu trả lời trước khi chấp nhận, nhưng tôi nghĩ rằng tôi có thể thực hiện công việc này.
ndurand

+1 để sửa lỗi. Bạn có thể loại bỏ nhu cầu sử dụng bất cứ thứ gì nhiều hơn một chồng, mà tôi nghĩ đơn giản hơn là xây dựng một cái cây.
Neil

2
@rdurand Một phần lợi thế của sửa lỗi sau có nghĩa là bạn không phải lo lắng về quyền ưu tiên (điều này đã được xem xét trước khi thêm nó vào ngăn xếp sau sửa chữa). Sau đó, bạn chỉ cần bật tất cả các toán hạng bạn tìm thấy cho đến khi bạn bật toán tử đầu tiên bạn tìm thấy trên ngăn xếp và sau đó đẩy lên ngăn xếp kết quả và bạn tiếp tục theo cách này cho đến khi bạn bật giá trị cuối cùng ra khỏi ngăn xếp.
Neil

1
@rdurand Biểu thức 2+4*6-3+7được chuyển đổi thành ngăn xếp sau sửa chữa + 7 - 3 + 2 * 4 6(trên cùng của ngăn xếp là đúng nhất). Bạn tắt 4 và 6 và áp dụng toán tử *, sau đó bạn đẩy 24 lại. Sau đó, bạn bật 24 và 2 và áp dụng toán tử +, sau đó bạn đẩy 26 trở lại. Bạn tiếp tục theo cách này và bạn sẽ thấy mình sẽ có câu trả lời đúng. Lưu ý rằng đó * 4 6là các điều khoản đầu tiên trên ngăn xếp. Điều đó có nghĩa là nó được thực hiện trước tiên vì bạn đã xác định quyền ưu tiên mà không yêu cầu dấu ngoặc đơn.
Neil

4

Câu trả lời của blubb là một khởi đầu tốt, nhưng ngữ pháp chính thức của anh ta tạo ra quá nhiều câu đối.

Đây là của tôi về nó:

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

Elà một biểu thức, Imột số nguyên và Mlà một biểu thức là một đối số cho phép toán nhân.


1
Mở rộng đẹp, cái này chắc chắn trông bớt bừa bộn!
blubb

Như tôi đã nhận xét về câu trả lời của blubb, tôi sẽ giữ một số dấu ngoặc đơn không mong muốn. Có thể làm cho ngẫu nhiên "ít ngẫu nhiên";) cảm ơn vì tiện ích bổ sung!
ndurand

3

Các dấu ngoặc trong biểu thức "cứng" biểu thị thứ tự đánh giá. Thay vì cố gắng tạo trực tiếp biểu mẫu được hiển thị, chỉ cần đưa ra một danh sách các toán tử theo thứ tự ngẫu nhiên và rút ra biểu mẫu hiển thị của biểu thức từ đó.

Số: 1 3 3 9 7 2

Toán tử: + * / + *

Kết quả: ((1 + 3) * 3 / 9 + 7) * 2

Xuất phát từ hình thức hiển thị là một thuật toán đệ quy tương đối đơn giản.

Cập nhật: đây là một thuật toán trong Perl để tạo biểu mẫu hiển thị. Bởi vì+*có tính phân phối, nó ngẫu nhiên sắp xếp thứ tự các thuật ngữ cho các toán tử đó. Điều đó giúp giữ cho các dấu ngoặc đơn từ tất cả xây dựng ở một bên.

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

Thuật toán này dường như luôn tạo ra các cây mất cân bằng: nhánh trái sâu, trong khi nhánh phải chỉ là một số duy nhất. Sẽ có quá nhiều parans mở trong việc năn nỉ từng biểu thức và thứ tự các thao tác luôn luôn từ trái sang phải.
scriptin

Cảm ơn câu trả lời của bạn, dan, nó giúp. Nhưng @scriptin, tôi không hiểu bạn không thích gì trong câu trả lời này? Bạn có thể giải thích một chút?
ndurand

@scriptin, có thể được sửa với sự ngẫu nhiên đơn giản của thứ tự hiển thị. Xem cập nhật.

@rdurand @ dan1111 Tôi đã thử kịch bản. Vấn đề của cây con lớn bên trái đã được sửa, nhưng cây được tạo ra vẫn rất mất cân bằng. Bức ảnh này cho thấy những gì tôi muốn nói. Điều này có thể không được coi là một vấn đề, nhưng nó dẫn đến tình huống các biểu (A + B) * (C + D)hiện phụ không bao giờ được trình bày trong các biểu thức được tạo ra, và cũng có rất nhiều parens lồng nhau.
scriptin

3
@scriptin, sau khi nghĩ về điều này, tôi đồng ý đây là một vấn đề.

2

Để mở rộng theo cách tiếp cận cây, giả sử mỗi nút là một biểu thức lá hoặc nhị phân:

Node := Leaf | Node Operator Node

Lưu ý rằng một chiếc lá chỉ là một số nguyên được tạo ngẫu nhiên ở đây.

Bây giờ, chúng ta có thể ngẫu nhiên tạo ra một cây. Quyết định xác suất mỗi nút là một chiếc lá cho phép chúng ta kiểm soát độ sâu dự kiến, mặc dù bạn cũng có thể muốn độ sâu tối đa tuyệt đối:

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

Sau đó, quy tắc đơn giản nhất để in cây là bọc () quanh mỗi biểu thức không có lá và tránh lo lắng về quyền ưu tiên của toán tử.


Ví dụ: nếu tôi ngoặc đơn biểu thức mẫu cuối cùng của bạn:

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

bạn có thể đọc cây sẽ tạo ra nó:

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

1

Tôi sẽ sử dụng cây. Họ có thể cung cấp cho bạn quyền kiểm soát tuyệt vời về việc tạo ra các biểu thức. Ví dụ: bạn có thể giới hạn độ sâu trên mỗi nhánh và chiều rộng của từng cấp riêng biệt. Thế hệ dựa trên cây cũng đưa ra câu trả lời đã có trong thế hệ, điều này rất hữu ích nếu bạn muốn đảm bảo rằng kết quả (và phần phụ) cũng đủ khó và / hoặc không quá khó để giải quyết. Đặc biệt nếu bạn thêm toán tử chia tại một số điểm, bạn có thể tạo các biểu thức đánh giá cho toàn bộ số.


Cảm ơn câu trả lời của bạn. Tôi đã có cùng một ý tưởng về cây, có thể đánh giá / kiểm tra các biểu hiện phụ. Có lẽ bạn có thể cung cấp thêm một chút chi tiết về giải pháp của bạn? Làm thế nào bạn sẽ xây dựng một cây như vậy (không thực sự như thế nào , nhưng cấu trúc chung sẽ là gì)?
ndurand

1

Đây là một chút khác biệt về câu trả lời tuyệt vời của Blubb:

Những gì bạn đang cố gắng xây dựng ở đây về cơ bản là một trình phân tích cú pháp hoạt động ngược lại. Vấn đề của bạn và trình phân tích cú pháp có điểm chung là ngữ pháp không ngữ cảnh , vấn đề này ở dạng 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> ')'

Các trình phân tích cú pháp bắt đầu bằng một luồng các thiết bị đầu cuối (mã thông báo theo nghĩa đen như 5hoặc *) và cố gắng lắp ráp chúng thành các số nguyên (các thứ bao gồm các thiết bị đầu cuối và các số khác, chẳng hạn như numberhoặc op). Vấn đề của bạn bắt đầu với các số nguyên tố và hoạt động ngược lại, chọn bất kỳ thứ gì giữa các ký hiệu "hoặc" (ống) một cách ngẫu nhiên khi gặp phải và lặp lại quy trình cho đến khi đến một thiết bị đầu cuối.

Một vài câu trả lời khác đã gợi ý rằng đây là một vấn đề về cây, nó dành cho một loại trường hợp hẹp nhất định trong đó không có trường hợp nào tham chiếu trực tiếp hoặc gián tiếp thông qua một nonterminal khác. Vì ngữ pháp cho phép điều đó, vấn đề này thực sự là một biểu đồ có hướng . (Tham chiếu gián tiếp thông qua một số khác cũng được tính vào điều này.)

Có một chương trình được gọi là Spew được xuất bản trên Usenet vào cuối những năm 1980 , ban đầu được thiết kế để tạo ra các tiêu đề lá cải ngẫu nhiên và cũng là một phương tiện tuyệt vời để thử nghiệm các "ngữ pháp đảo ngược" này. Nó hoạt động bằng cách đọc một mẫu chỉ đạo việc sản xuất một dòng thiết bị đầu cuối ngẫu nhiên. Vượt xa giá trị giải trí của nó (tiêu đề, bài hát đồng quê, tiếng Anh có thể phát âm được), tôi đã viết rất nhiều mẫu hữu ích để tạo dữ liệu thử nghiệm có phạm vi từ văn bản đơn giản sang XML thành chính xác về mặt cú pháp nhưng không thể dịch được C. Mặc dù đã 26 tuổi và được viết bằng K & R C và có định dạng mẫu xấu, nó sẽ biên dịch tốt và hoạt động như quảng cáo. Tôi đã tạo ra một mẫu giải quyết vấn đề của bạn và đăng nó lên pastebin kể từ khi thêm nhiều văn bản ở đây có vẻ không phù hợp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.