用java解决百度之星移动火柴的问题 part 2

阅读更多

The next task is to evaluate a given expression. A simple way is to build a generic evaluator for the simple math expression we are dealing with. In fact, building a parser for +, -, /, * and ^(for powers) is as much work as building one for just + and -. The references we are using here are:

  • http://www.smccd.net/accounts/hasson/C++2Notes/ArithmeticParsing.html
  • http://www.chris-j.co.uk/parsing.php

There are tons of references on the language parsing, but these are enough for us to build a simple one. Again, the code is under 200 lines.

import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;

public class NumericExprParser
{
    public double evaluate(String expr)
    {
        Queue postFixExpr = convertToPostFixExpr(expr);
        return evaluatePostFixExpr(postFixExpr);
    }

    private double evaluatePostFixExpr(Queue queue)
    {
        Stack stack = new Stack();
        while (!queue.isEmpty())
        {
            String t = queue.remove();
            if (isNumber(t))
            {
                stack.push(Double.parseDouble(t));
            }
            else if (isOperator(t))
            {
                Double n1 = stack.pop();
                Double n2 = stack.pop(); // for unary operators, this is empty.

                if (t.equals("+")) stack.push(n2 + n1);
                else if (t.equals("-")) stack.push(n2 - n1);
                else if (t.equals("*")) stack.push(n2 * n1);
                else if (t.equals("/")) stack.push(n2 / n1);
                else if (t.equals("^")) stack.push(Math.pow(n2, n1));
            }
        }

        return stack.pop();
    }

    private Queue convertToPostFixExpr(String expr)
    {
        Queue queue = new LinkedList();
        Stack stack = new Stack();

        String[] tokens = getTokens(expr);
        for (String s : tokens)
        {
            if (isNumber(s))
            {
                queue.add(s);
            }
            else if (isOperator(s))
            {
                while (!stack.isEmpty() && isOperator(stack.peek()) && isSameOrHigherPriority(stack.peek(), s))
                {
                    String t = stack.pop();
                    queue.add(t);
                }

                stack.push(s);
            }
            else if (s.equals("("))
            {
                stack.push(s);
            }
            else if (s.equals(")"))
            {
                String t;
                while (!(t = stack.pop()).equals("("))
                {
                    queue.add(t);
                }
            }
        }

        while (!stack.isEmpty())
        {
            String t = stack.pop();
            queue.add(t);
        }

        return queue;
    }

    private boolean isNumber(String s)
    {
        try
        {
            Double.parseDouble(s);
            return true;
        }
        catch (Exception ex)
        {
            return false;
        }
    }

    private boolean isOperator(String s)
    {
        return s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/") || s.equals("^");
    }

    private boolean isSameOrHigherPriority(String s, String t)
    {
        if (t.equals("+") || t.equals("-"))
        {
            return true;
        }
        else if (t.equals("*") || t.equals("/"))
        {
            return s.equals("*") || s.equals("/") || s.equals("^");
        }
        else if (t.equals("^"))
        {
            return s.equals("^");
        }
        else return false;
    }

    private String[] getTokens(String expr)
    {
        List tokens = new ArrayList();

        StringTokenizer st = new StringTokenizer(expr, "+-*/^()", true);
        while (st.hasMoreTokens())
        {
            String s = st.nextToken().trim();
            if (!s.equals(""))
            {
                tokens.add(s);
            }
        }

        return tokens.toArray(new String[0]);
    }
}

 

This parser can handle +, -, /, *, ^ and (). Since we don't need unary -, it's not there. The parentheses is for () only, since we don't need [] and {}.

 

This class is sufficient for our purpose. A test case is like this:

import junit.framework.TestCase;

public class NumericExprParserTest extends TestCase
{
    private NumericExprParser exprParser = new NumericExprParser();

    public void testEvaluate()
    {
        String expr = "(10 - 1) * 2 / 3 ^ 2";
        double res = exprParser.evaluate(expr);
        System.out.println(res);
        assertTrue(res == 2);

        expr = "2 * 3 + 5 * 4 / 2^2";
        res = exprParser.evaluate(expr);
        System.out.println(res);
        assertTrue(res == 11);

        expr = "0 -5 * 3^2";
        res = exprParser.evaluate(expr);
        System.out.println(res);
        assertTrue(res == -45);
    }
}

 

Note that this class is a reusable class, not just for our problem here.

 

With these building blocks, we are ready to attack the problem. There are several steps:

  • We need to check whether the original one is already a solution, such as 1 + 1 = 2. This is because once we take a match off and it's not a number anymore, then we lose this solution. Otherwise, we just loop through all possibilities and disgard any case where we can't form numbers.
  • Given a string, we convert the digits into MatchDigit AND we keep a reference from MatchDigit to the original digit so that once we have a new expression in matches, we can convert them back to strings in order to use the evaluator to evaluate.
  • Once we have the MatchDigit list, we can loop through all possibilities to find the solutions. In here, I just print out the solutions. A better way would be to construct some kind of result class to return the results, but I leave that out for simplicity without much harm.

The class is as follows:

 

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Given an arithmetic expression, by moving one match to make it a valid equation.
 */
public class MatchDigitEquatorSolver
{
    private static final String DIGITS = "0123456789";

    public void solve(String expr)
    {
        // check the original expression
        if (checkResult(expr))
        {
            System.out.println("The original expr is a solution: expr: " + expr);
        }
        
        HashMap indices = new HashMap();

        ArrayList mds = new ArrayList();

        for (int i=0; i -1;
    }

    private boolean checkResult(String expr)
    {
        String[] exprs = expr.split("=");

        NumericExprParser parser = new NumericExprParser();
        double left = parser.evaluate(exprs[0]);
        double right = parser.evaluate(exprs[1]);

        return left == right;
    }
}

 

Although there are still 4 for loops, they are more managable in the context as they are close to the pseudo code we present at the beginning, i.e., they are aligned with the business logic. A test case is like this:

 

 

import junit.framework.TestCase;

public class MatchDigitEquatorSolverTest extends TestCase
{
    private MatchDigitEquatorSolver solver = new MatchDigitEquatorSolver();

    public void testSimple()
    {
        String a = "9 + 5 = 9";
        solver.solve(a);
    }

    public void testOriginal()
    {
        String a = "4 + 5 = 9";
        solver.solve(a);
    }

    public void testMoreDigits()
    {
        String a = "19 + 5 = 19";
        solver.solve(a);
    }

    public void testParen()
    {
        String a = "2 * (9 + 5) = 18";
        solver.solve(a);
    }
}

 

Note that in our code there is no restriction to single digit number, as shown in the 3rd case in the above. And we allow ^(power) operators as well.

 

 The results are like this:

expr: 9 + 5 = 9
new expr: 3 + 6 = 9
move match UPPERLEFT from digit[index: 0] 9 to the position LOWERLEFT in the digit[index: 4] 5.
expr: 9 + 5 = 9
new expr: 3 + 5 = 8
move match UPPERLEFT from digit[index: 0] 9 to the position LOWERLEFT in the digit[index: 8] 9.
The original expr is a solution: expr: 4 + 5 = 9
expr: 19 + 5 = 19
new expr: 13 + 6 = 19
move match UPPERLEFT from digit[index: 1] 9 to the position LOWERLEFT in the digit[index: 5] 5.
expr: 19 + 5 = 19
new expr: 13 + 5 = 18
move match UPPERLEFT from digit[index: 1] 9 to the position LOWERLEFT in the digit[index: 10] 9.
expr: 2 * (9 + 5) = 18
new expr: 2 * (3 + 6) = 18
move match UPPERLEFT from digit[index: 5] 9 to the position LOWERLEFT in the digit[index: 9] 5.

 

 Now we are done, like we do college homework(meaning we throw them away once we get the grades). But as professional software engineers, we always wonder what's next because we need to maintain "the damn thing". What if we change our requirement, say, we can now move two matches? Or allow more operations. How flexible is our class structure to handle these changes? In the first case, we just need to write a new solver class and reuse the MatchDigit and parser classes. In the second case, we just modify the parser class or write a completely new one.

 

Normally, a stateless operation has less entanglement than stateful operations. For example, writing a parser for, say java, language is much harder than this example because parsing tokens heavily depends on the token series and the current position in the series, e.g., the parentheses handling, the math functions and their arguments handling.

 

Think, think, think ...

 

你可能感兴趣的:(百度,Java,junit,C,C++)