简体   繁体   English

用Java构建数学游戏

[英]Building a math game in Java

I am building a math game for java and I'm stuck at this part as per the details of my assignment. 我正在为java构建一个数学游戏,并且根据我的任务细节,我被困在这个部分。 The rules are simple: you have to use each number only once and only the 4 numbers that were read from the user to find one equation to obtain 24. 规则很简单:你必须只使用每个数字一次,只使用从用户读取的4个数字来找到一个方程式来获得24。

For example, for the numbers 4,7,8,8, a possible solution is: (7-(8/8))*4=24. 例如,对于数字4,7,8,8,可能的解决方案是:(7-(8/8))* 4 = 24。

Most sets of 4 digits can be used in multiple equations that result in 24. For example the input 2,2,4,7 can be used in multiple ways to obtain 24: 大多数4位数组可用于导致24的多个方程中。例如,输入2,2,4,7可以多种方式使用以获得24:

2+2*(4+7) = 24 2 + 2 *(4 + 7)= 24

2+2*(7+4) = 24 2 + 2 *(7 + 4)= 24

(2+2)*7-4 = 24 (2 + 2)* 7-4 = 24

(2*2)*7-4 = 24 (2 * 2)* 7-4 = 24

2*(2*7)-4 = 24 2 *(2 * 7)-4 = 24

There are also combinations of 4 numbers that cannot result into any equation equal with 24. For example 1,1,1,1. 还有4个数字的组合不能导致任何等于24的等式。例如1,1,1,1。 In this case, your program should return that there is no possible equation equal with 24. 在这种情况下,您的程序应该返回没有可能等于24的等式。

Note: Although we will enter 4 integers between 1 and 9, we will use doubles to compute all the operations. 注意:虽然我们将在1到9之间输入4个整数,但我们将使用双精度来计算所有操作。 For example, the numbers 3,3,8,8 can be combined into the formula: 8/(3-8/3) = 24. 例如,数字3,3,8,8可以组合成公式:8 /(3-8 / 3)= 24。

Workflow: Your program should read the 4 numbers from the user and output a formula that results in 24. The algorithm should enumerate all the possible orders of 4 numbers, all the possible combinations and all the possible formulas. 工作流程:您的程序应该读取用户的4个数字并输出一个导致24的公式。算法应该枚举4个数字的所有可能顺序,所有可能的组合和所有可能的公式。

Which leads me to 24 permutations of Numbers a,b,c,d and 64 permutations of operators +-/* . 这导致我对数字a,b,c,d和运算符的64个排列的24个排列+-/* How I came to this conclusion was 4^3 4 operators only 3 fill spots in the equation. 我如何得出这个结论是4 ^ 3 4个算子只有3个填充点在等式中。 Except today I am having trouble writing the evaluation method and also accounting for parentases in the equations. 除了今天,我在编写评估方法时遇到问题,并且还要考虑方程中的父类。

Here is my code: 这是我的代码:

public static void evaluate(cbar [][] operations , double [][] operands)
{
    /*
    This is the part that gets me how am I supposed to account
    for parentases and bring all these expressions togather to
    actually form and equation.
    */
}

This problem presents several challenges. 这个问题带来了一些挑战。 My solution below is about two hundred lines long. 我的解决方案大约有两百行。 It's probably a little longer than the assignment requires because I generalized it to any number of terms. 这可能比任务需要的时间长一点,因为我将它推广到任意数量的术语。 I encourage you to study the algorithm and write your own solution. 我鼓励您研究算法并编写自己的解决方案。

The main obstacles we must overcome are the following. 我们必须克服的主要障碍如下。

  • How do we generate permutations without repetition? 我们如何在不重复的情况下生成排列?

  • How do we build and evaluate arithmetic expressions? 我们如何构建和评估算术表达式?

  • How do we convert the expressions into unique strings? 我们如何将表达式转换为唯一的字符串?

There are many ways to generate permutations. 有许多方法可以生成排列。 I chose a recursive approach because it's easy to understand. 我选择了一种递归方法,因为它很容易理解。 The main complication is that terms can be repeated, which means that there may be fewer than 4! = 4*3*2*1 主要的复杂因素是术语可以重复,这意味着可能少于4! = 4*3*2*1 4! = 4*3*2*1 permutations. 4! = 4*3*2*1排列。 For example, if the terms are 1 1 1 2 , there are only four permutations. 例如,如果术语是1 1 1 2 ,则只有四个排列。

To avoid duplicating permutations, we start by sorting the terms. 为避免重复排列,我们首先对术语进行排序。 The recursive function finds places for all duplicate terms from left to right without backtracking. 递归函数从左到右查找所有重复项的位置,而不进行回溯。 For example, once the first 1 has been placed in the array, all the remaining 1 terms are placed to the right of it. 例如,一旦将第一个1放入数组中,所有剩余的1术语都放在它的右侧。 But when we get to a 2 term, we can go back to the beginning of the array. 但是当我们达到2学期时,我们可以回到数组的开头。

To build arithmetic expressions, we use another recursive function. 为了构建算术表达式,我们使用另一个递归函数。 This function looks at each position between two terms of the permutation, splitting the array into a segment to the left of the position and a segment to the right. 此函数查看排列的两个项之间的每个位置,将数组拆分为位置左侧的段和右侧的段。 It makes a pair of recursive calls to build expressions for the left and right segments. 它进行一对递归调用以构建左右段的表达式。 Finally, it joins the resulting child expressions with each of the four arithmetic operators. 最后,它将结果子表达式与四个算术运算符中的每一个连接起来。 The base case is when the array is of size one, so it can't be split. 基本情况是当数组大小为1时,因此无法拆分。 This results in a node with no operator and no children, only a value. 这导致节点没有运算符且没有子节点,只有一个值。

Evaluating the expressions by performing arithmetic on double values would be problematic due to the imprecision of floating-point division. 由于浮点除法的不精确性,通过对double值执行算术来评估表达式将是有问题的。 For example, 1.0 / 3 = 0.33333... , but 3 * 0.33333... = 0.99999... . 例如, 1.0 / 3 = 0.33333... ,但是3 * 0.33333... = 0.99999... This makes it difficult to know for sure that 1 / 3 * 3 = 1 when you're using double values. 这使得当您使用double值时很难确定1 / 3 * 3 = 1 To avoid these difficulties, I defined a Fraction class. 为了避免这些困难,我定义了一个Fraction类。 It performs arithmetic operations on fractions and always simplifies the result by means of the greatest common divisor. 它对分数执行算术运算,并始终通过最大公约数来简化结果。 Division by zero does not result in an error message. 除以零不会导致错误消息。 Instead, we store the fraction 0/0. 相反,我们存储分数0/0。

The final piece of the puzzle is converting expressions into strings. 拼图的最后一部分是将表达式转换为字符串。 We want to make canonical or normalized strings so that we don't repeat ourselves needlessly. 我们想制作规范或规范化的字符串,这样我们就不会不必要地重复自己。 For example, we don't want to display 1 + (1 + (1 + 2)) and ((1 + 1) + 1) + 2 , since these are essentially the same expression. 例如,我们不希望显示1 + (1 + (1 + 2))((1 + 1) + 1) + 2 ,因为它们基本上是相同的表达式。 Instead of showing all possible parenthesizations, we just want to display 1 + 1 + 1 + 2 . 我们只想显示1 + 1 + 1 + 2 ,而不是显示所有可能的括号。

We can achieve this by adding parentheses only when necessary. 我们可以通过仅在必要时添加括号来实现此目的。 To wit, parentheses are necessary if a node with a higher-priority operator (multiplication or division) is the parent of a node with a lower-priority operator (addition or subtraction). 也就是说,如果具有较高优先级运算符(乘法或除法)的节点是具有较低优先级运算符(加法或减法)的节点的父节点,则必须使用括号。 By priority I mean operator precedence, also known as the order of operations. 优先级是指运算符优先级,也称为运算顺序。 The higher-priority operators bind more tightly than the lower ones. 优先级较高的运算符比较低优先级的运算符绑定更紧密。 So if a parent node has higher priority than the operator of a child node, it is necessary to parenthesize the child. 因此,如果父节点的优先级高于子节点的运算符,则必须为子节点括号。 To ensure that we end up with unique strings, we check them against a hash set before adding them to the result list. 为了确保我们最终得到唯一的字符串,我们会在将它们添加到结果列表之前根据哈希集检查它们。

The following program, Equation.java , accepts user input on the command line. Equation.java的以下程序接受命令行上的用户输入。 The parameters of the game are on the first line of the Equation class. 游戏的参数位于Equation类的第一行。 You can modify these to build expressions with more terms, bigger terms, and different target values. 您可以修改这些以构建具有更多术语,更大术语和不同目标值的表达式。

import java.lang.*;
import java.util.*;
import java.io.*;

class Fraction {                  // Avoids floating-point trouble.
  int num, denom;
  static int gcd(int a, int b) {  // Greatest common divisor.
    while (b != 0) {
      int t = b;
      b = a % b;
      a = t;
    }
    return a;
  }
  Fraction(int num, int denom) {  // Makes a simplified fraction.
    if (denom == 0) {             // Division by zero results in
      this.num = this.denom = 0;  //  the fraction 0/0. We do not
    } else {                      //  throw an error.
      int x = Fraction.gcd(num, denom);
      this.num = num / x;
      this.denom = denom / x;     
    }
  }
  Fraction plus(Fraction other) {
    return new Fraction(this.num * other.denom + other.num * this.denom,
        this.denom * other.denom);
  }
  Fraction minus(Fraction other) {
    return this.plus(new Fraction(-other.num, other.denom));
  }
  Fraction times(Fraction other) {
    return new Fraction(this.num * other.num, this.denom * other.denom);
  }
  Fraction divide(Fraction other) {
    return new Fraction(this.num * other.denom, this.denom * other.num);
  }
  public String toString() {      // Omits the denominator if possible.
    if (denom == 1) {
      return ""+num;
    }
    return num+"/"+denom;
  }
}

class Expression {                // A tree node containing a value and
  Fraction value;                 //  optionally an operator and its
  String operator;                //  operands.
  Expression left, right;
  static int level(String operator) {
    if (operator.compareTo("+") == 0 || operator.compareTo("-") == 0) {
      return 0;                   // Returns the priority of evaluation,
    }                             //  also known as operator precedence
    return 1;                     //  or the order of operations.
  }
  Expression(int x) {             // Simplest case: a whole number.
    value = new Fraction(x, 1);
  }
  Expression(Expression left, String operator, Expression right) {
    if (operator == "+") {
      value = left.value.plus(right.value);
    } else if (operator == "-") {
      value = left.value.minus(right.value);
    } else if (operator == "*") {
      value = left.value.times(right.value);
    } else if (operator == "/") {
      value = left.value.divide(right.value);
    }
    this.operator = operator;
    this.left = left;
    this.right = right;
  }
  public String toString() {      // Returns a normalized expression,
    if (operator == null) {       //  inserting parentheses only where
      return value.toString();    //  necessary to avoid ambiguity.
    }
    int level = Expression.level(operator);
    String a = left.toString(), aOp = left.operator,
           b = right.toString(), bOp = right.operator;
    if (aOp != null && Expression.level(aOp) < level) {
      a = "("+a+")";              // Parenthesize the child only if its
    }                             //  priority is lower than the parent's.
    if (bOp != null && Expression.level(bOp) < level) {
      b = "("+b+")";
    }
    return a + " " + operator + " " + b;
  }
}

public class Equation {

  // These are the parameters of the game.
  static int need = 4, min = 1, max = 9, target = 24;

  int[] terms, permutation;
  boolean[] used;
  ArrayList<String> wins = new ArrayList<String>();
  Set<String> winSet = new HashSet<String>();
  String[] operators = {"+", "-", "*", "/"};

  // Recursively break up the terms into left and right
  //  portions, joining them with one of the four operators.
  ArrayList<Expression> make(int left, int right) {
    ArrayList<Expression> result = new ArrayList<Expression>();
    if (left+1 == right) {
      result.add(new Expression(permutation[left]));
    } else {
      for (int i = left+1; i < right; ++i) {
        ArrayList<Expression> leftSide = make(left, i);
        ArrayList<Expression> rightSide = make(i, right);
        for (int j = 0; j < leftSide.size(); ++j) {
          for (int k = 0; k < rightSide.size(); ++k) {
            for (int p = 0; p < operators.length; ++p) {
              result.add(new Expression(leftSide.get(j),
                    operators[p],
                    rightSide.get(k)));
            }
          }
        }
      }
    }
    return result;
  }

  // Given a permutation of terms, form all possible arithmetic
  //  expressions. Inspect the results and save those that
  //  have the target value.
  void formulate() {
    ArrayList<Expression> expressions = make(0, terms.length);
    for (int i = 0; i < expressions.size(); ++i) {
      Expression expression = expressions.get(i);
      Fraction value = expression.value;
      if (value.num == target && value.denom == 1) {
        String s = expressions.get(i).toString();
        if (!winSet.contains(s)) {// Check to see if an expression
          wins.add(s);            //  with the same normalized string
          winSet.add(s);          //  representation was saved earlier.
        }
      }
    }
  }

  // Permutes terms without duplication. Requires the terms to
  //  be sorted. Notice how we check the next term to see if
  //  it's the same. If it is, we don't return to the beginning
  //  of the array.
  void permute(int termIx, int pos) {
    if (pos == terms.length) {
      return;
    }
    if (!used[pos]) {
      permutation[pos] = terms[termIx];
      if (termIx+1 == terms.length) {
        formulate();
      } else {
        used[pos] = true;
        if (terms[termIx+1] == terms[termIx]) {
          permute(termIx+1, pos+1);
        } else {
          permute(termIx+1, 0);
        }
        used[pos] = false;
      }
    }
    permute(termIx, pos+1);
  }

  // Start the permutation process, count the end results, display them.
  void solve(int[] terms) {
    this.terms = terms;           // We must sort the terms in order for
    Arrays.sort(terms);           //  the permute() function to work.
    permutation = new int[terms.length];
    used = new boolean[terms.length];
    permute(0, 0);
    if (wins.size() == 0) {
      System.out.println("There are no feasible expressions.");
    } else if (wins.size() == 1) {
      System.out.println("There is one feasible expression:");
    } else {
      System.out.println("There are "+wins.size()+" feasible expressions:");
    }
    for (int i = 0; i < wins.size(); ++i) {
      System.out.println(wins.get(i) + " = " + target);
    }
  }

  // Get user input from the command line and check its validity.
  public static void main(String[] args) {
    if (args.length != need) {
      System.out.println("must specify "+need+" digits");
      return;
    }
    int digits[] = new int[need];
    for (int i = 0; i < need; ++i) {
      try {
        digits[i] = Integer.parseInt(args[i]);
      } catch (NumberFormatException e) {
        System.out.println("\""+args[i]+"\" is not an integer");
        return;
      }
      if (digits[i] < min || digits[i] > max) {
        System.out.println(digits[i]+" is outside the range ["+
            min+", "+max+"]");
        return;
      }
    }
    (new Equation()).solve(digits);
  }
}

I would recommend you to use a tree structure to store the equation, ie a syntactic tree in which the root represents and operator having two children representing the operands and so on recursively. 我建议你使用树结构来存储方程,即根表示的语法树和有两个子节点表示操作数的运算符,依此类推。 You would probably get a cleaner code doing it like that, because then you won't need to generate the combinations of operands "by hand", but you can make a code which picks every operand from a 1-dimensional char[] operands = new char[] {'+','-','*','/'} array. 你可能会得到一个更干净的代码,因为那时你不需要“手动”生成操作数的组合,但是你可以创建一个从1维char []操作数中选择每个操作数的代码= new char [] {'+',' - ','*','/'}数组。

If you don't want to use a syntactic tree or think it's not necessary for your use case you can always try to find a different way to make the code to pick operands from the 1-dimensional array and store them into a different data structure. 如果您不想使用语法树或认为它不是您的用例所必需的,您可以总是尝试找到一种不同的方法来使代码从一维数组中选取操作数并将它们存储到不同的数据结构中。 But I would especially avoid writing all the combinations as you are doing. 但我会特别避免像你一样编写所有组合。 It does not look very easy to maintain. 它看起来不容易维护。

I have fixed the similar puzzle with below code. 我用下面的代码修复了类似的谜题。

public static boolean game24Points(int[] operands) {
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine engine = sem.getEngineByName("javascript");

    char[] operations = new char[] { '+', '-', '*', '/' };
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            for (int k = 0; k < 4; k++) {
                try {
                    String exp = "" + operands[0] + operations[i] + operands[1] + operations[j]
                            + operands[2] + operations[k] + operands[3];
                    String res = engine.eval(exp).toString();
                    if (Double.valueOf(res).intValue() == 24) {
                        System.out.println(exp);
                        return true;
                    }
                } catch (ScriptException e) {
                    return false;
                }
            }
        }
    }
    return false;
}

Here are testcases 这是测试用例

public void testCase01() {
    int[] operands = { 7, 2, 1, 10 };
    assertEquals(true, Demo.game24Points(operands));
}

public void testCase02() {
    int[] operands = { 1, 2, 3, 4 };
    assertEquals(true, Demo.game24Points(operands));
}

public void testCase03() {
    int[] operands1 = { 5, 7, 12, 12 };
    assertEquals(true, Demo.game24Points(operands1));
    int[] operands = { 10, 3, 3, 23 };
    assertEquals(true, Demo.game24Points(operands));
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM