生成具有错误答案的随机方程(运算符优先级问题)

Generating random equations with wrong answers ( operator precedence problem )

提问人:Loso 提问时间:6/19/2023 更新时间:6/20/2023 访问量:63

问:

我正在尝试生成一个随机方程及其答案。 一切正常,除了答案计算错误(不考虑运算符优先级)。一段时间以来,我一直在尝试修复它,我对如何处理它有一个大致的了解,但我不知道如何实际操作,而且网络上几乎没有关于它的信息。

这是我的代码:

    public static Equation randomEquation() {
        Random random = new Random();
        int numNumbers = random.nextInt(3) + 2; // generate between 2 and 4 numbers
        List<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < numNumbers; i++) {
            int number = random.nextInt(101); // generate random number between 0 and 100
            numbers.add(number);
        }

        StringBuilder equationBuilder = new StringBuilder();
        int answer = numbers.get(0);
        equationBuilder.append(numbers.get(0));
        for (int i = 1; i < numNumbers; i++) {
            int operator = random.nextInt(4); // generate random operator: 0 for addition, 1 for subtraction, 2 for multiplication, 3 for division
            int number;
            if (operator == 2) {
                // Limit multiplication operation to never generate a result more than 10
                do {
                    number = random.nextInt(11);
                } while (number == 0);
            } else {
                number = random.nextInt(101); // generate random number between 0 and 100
            }

            switch (operator) {
                case 0 -> {
                    equationBuilder.append(" + ");
                    answer += number;
                }
                case 1 -> {
                    equationBuilder.append(" - ");
                    answer -= number;
                }
                case 2 -> {
                    equationBuilder.append(" * ");
                    answer *= number;
                }
                case 3 -> {
                    equationBuilder.append(" / ");
                    if (number == 0 || answer % number != 0) {
                        // If the second number is 0 or the division results in a non-integer answer, regenerate the equation
                        return randomEquation();
                    } else {
                        answer /= number;
                    }
                }
            }

            equationBuilder.append(number);
        }

        String equation = equationBuilder.toString();

        return new Equation(equation, answer);
    }

因此,这里的答案显然存在问题:

switch (operator) {
                case 0 -> {
                    equationBuilder.append(" + ");
                    answer += number;
                }
                case 1 -> {
                    equationBuilder.append(" - ");
                    answer -= number;
                }
                case 2 -> {
                    equationBuilder.append(" * ");
                    answer *= number;
                }
                case 3 -> {
                    equationBuilder.append(" / ");
                    if (number == 0 || answer % number != 0) {
                        // If the second number is 0 or the division results in a non-integer answer, regenerate the equation
                        return randomEquation();
                    } else {
                        answer /= number;
                    }
                }
            }

我只是在每个数字后面附加答案,这在乘法和除法时没有意义。

所以我的问题是,如何将运算符优先级的概念引入我的代码中?

Java 数学 随机 运算符 算符优先级

评论

0赞 Sweeper 6/19/2023
每次都只引入括号怎么样?这样,您就无需担心操作员优先级。
0赞 Loso 6/19/2023
仅仅引入括号是行不通的,我仍然需要手动计算它(我已经尝试过了)
0赞 Sweeper 6/19/2023
我问您是否同意到处添加括号的输出。
0赞 Sam Mason 6/19/2023
递归通常是对此进行建模的自然方式。例如,创建一个函数来创建一个新的二进制运算符,该运算符调用自身来获取它所操作的表达式。最初,我只是在任何地方都放括号,以清楚地表明发生了什么。一旦你开始工作,该函数也可以返回一个“运算符优先级”,该优先级可用于删除多余的大括号

答:

1赞 Sweeper 6/19/2023 #1

为了说明运算符的优先级,不要将等式视为“作为运算符分隔的数字列表”。相反,把它想象成一棵树

定义一个 ,它可以是:EquationComponent

  • 常数,或
  • 对另外两个 S 的运算(加号、减号、乘号、除号)EquationComponent

这种密封的层次结构可以由密封的接口/类来实现。如果你的 Java 版本没有这个功能,那么常规接口也可以。

一个可以做的特别之处在于它有一个价值,或者你想称之为“答案”。EquationComponent

要从常数中获得“答案”,只需返回该数即可。若要从操作中获取“答案”,首先从操作的两端获取“答案”,然后将运算符应用于这两个值。

当我们将 an 转换为字符串时,我们总是在它周围添加括号。而且,由于“答案”是根据方程式分量计算的,因此可以确保答案始终与方程式的显示方式一致。EquationComponent

下面是一个示例实现。注意如何实施和实施。toStringgetAnswer

enum Operation {
    PLUS("+"),
    MINUS("-"),
    TIMES("*"),
    DIVIDE("/");
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }
}

sealed interface EquationComponent permits Equation, ConstantNumber {
    int getAnswer();
}

final class ConstantNumber implements EquationComponent {
    private final int number;

    // getters, equals, hashCode if you need those...

    public ConstantNumber(int number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return Integer.toString(number);
    }

    @Override
    public int getAnswer() {
        return number;
    }
}

final class Equation implements EquationComponent {
    private final EquationComponent left;
    private final EquationComponent right;
    private final Operation operation;

    // getters, equals, hashCode if you need those...

    public Equation(EquationComponent left, EquationComponent right, Operation operation) {
        this.left = left;
        this.right = right;
        this.operation = operation;
    }

    // I'm using string concatenation here just for convenience,
    // you can totally change this to use StringBuilder instead.
    @Override
    public String toString() {
        // note that this adds parentheses to every operation
        return "(" + left + " " + operation + " " + right + ")";
    }

    @Override
    public int getAnswer() {
        // this could be cached somewhere...
        return switch (operation) {
            case PLUS -> left.getAnswer() + right.getAnswer();
            case MINUS -> left.getAnswer() - right.getAnswer();
            case TIMES -> left.getAnswer() * right.getAnswer();
            // integer division?
            case DIVIDE -> left.getAnswer() / right.getAnswer();
        };
    }
}

现在我们有了基础设施,我们可以开始生成方程式了。而不是决定你要生成多少个数字。确定要生成多少个操作

编写生成操作的方法。此方法将选择一个运算符,然后对于它的左侧和右侧,递归调用自身以生成更多操作,如果已经生成了最大操作数,则调用常量数。

private static int remainingOperations;
private static final Random r = new Random();

public static void main(String[] value) throws IOException {
    remainingOperations = r.nextInt(1, 4); // 1 to 3 operations
    EquationComponent eq = makeEquation();
    System.out.println(eq);
    System.out.println(eq.getAnswer());
}

private static EquationComponent makeEquation() {
    if (remainingOperations > 0) {
        remainingOperations--;
        // even if your version of Java doesn't have switch expressions
        // it should be obvious what this does and converting it to a switch
        // statement is trivial.
        Operator op = switch (r.nextInt(4)) {
            case 0 -> Operator.PLUS;
            case 1 -> Operator.MINUS;
            case 2 -> Operator.TIMES;
            default -> Operator.DIVIDE;
        };
        if (op == Operator.TIMES) {
            return new Operation(
                    // the left could be another number
                    r.nextBoolean() ? makeEquation() : makeNumber(101),
                    // generate a small number on the right if it is multiplication
                    makeNumber(11),
                    op
            );
        } else {
            // this allows both sides to be a constant number,
            // which means we could generate an equation with fewer
            // operations than remainingOperations
            return new Operation(
                    // the left could be another number
                    r.nextBoolean() ? makeEquation() : makeNumber(101),
                    // the right could be another number
                    r.nextBoolean() ? makeEquation() : makeNumber(101),
                    op
            );
        }
    } else {
        return makeNumber(101);
    }
}

private static ConstantNumber makeNumber(int limit) {
    return new ConstantNumber(r.nextInt(limit));
}
1赞 Reilas 6/20/2023 #2

由于运算符是随机生成的,因此必须在生成 equationBuilder 字符串后对其进行评估。

在此示例中,我利用 PatternMatcher 类,使用正则表达式模式来隔离运算索引。

static int answer(String string) {
    Pattern pattern = Pattern.compile("(-?\\d+) \\* (-?\\d+)");
    Matcher matcher;
    int operandA, operandB, result;
    while ((matcher = pattern.matcher(string)).find()) {
        operandA = Integer.parseInt(matcher.group(1));
        operandB = Integer.parseInt(matcher.group(2));
        result = operandA * operandB;
        string = string.replace(matcher.group(), String.valueOf(result));
    }
    pattern = Pattern.compile("(-?\\d+) / (-?\\d+)");
    while ((matcher = pattern.matcher(string)).find()) {
        operandA = Integer.parseInt(matcher.group(1));
        operandB = Integer.parseInt(matcher.group(2));
        result = operandA / operandB;
        string = string.replace(matcher.group(), String.valueOf(result));
    }
    pattern = Pattern.compile("(-?\\d+) ([-+]) (-?\\d+)");
    while ((matcher = pattern.matcher(string)).find()) {
        operandA = Integer.parseInt(matcher.group(1));
        operandB = Integer.parseInt(matcher.group(3));
        result = switch (matcher.group(2)) {
            case "+" -> operandA + operandB;
            default -> operandA - operandB;
        };
        string = string.replace(matcher.group(), String.valueOf(result));
    }
    return Integer.parseInt(string);
}

而且,我修改了您的运算符分配代码块,如下所示。

switch (operator) {
    case 0 -> equationBuilder.append(" + ");
    case 1 -> equationBuilder.append(" - ");
    case 2 -> equationBuilder.append(" * ");
    case 3 -> {
        equationBuilder.append(" / ");
        if (number == 0 || answer % number != 0) {
            // If the second number is 0 or the division results in a non-integer answer, regenerate the equation
            return randomEquation();
        }
    }
}

随后,将其作为值返回。

return new Equation(equation, answer(equation));

这里有几个例子。

6 * 5 - 34 - 59 = -63
8 * 1 = 8
5 * 7 = 35
71 - 22 = 49
43 - 52 = -9

此外,我还向 while-loops 添加了一些 println 调用,以显示过程。

15 * 5
  15 * 5 = 75

100 + 47 - 40 + 67
  100 + 47 = 147
  147 - 40 = 107
  107 + 67 = 174

87 / 1 * 4
  1 * 4 = 4
  87 / 4 = 21

22 * 6 - 37
  22 * 6 = 132
  132 - 37 = 95

17 - 44 + 45 * 6
  45 * 6 = 270
  17 - 44 = -27
  -27 + 270 = 243

此外,您可以通过使用双精度值并调整正则表达式以匹配实数来改进此代码。

甚至,利用 BigDecimal 类来计算 64 位范围之外的数字。