leetcode-为运算表达式设计优先级

151 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招系列活动-刷题打卡任务,点击查看活动详情

题目描述

给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。

示例 1:
输入:expression = "2-1-1"
输出:[0,2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2

示例 2:
输入:expression = "23-45"
输出:[-34,-14,-10,-10,10]
解释:
(2*(3-(45))) = -34
((2
3)-(45)) = -14
((2
(3-4))5) = -10
(2
((3-4)5)) = -10
(((2
3)-4)*5) = 10

思路分析

这题虽然归在“动态规划”的类别中,但是一开始也没想到怎么定义状态和写出状态转移方程。尝试了一下带备忘录的递归,也AC过了,后来看了其他人的题解,才对动态规划的方法豁然开朗,这里把2种思路都记录一下。

递归

在expression中,把数字记为num,把操作符记为op,那么expression就是数字和操作符交替出现的状态,例如 递归1.png 那么,任何一个操作符,都可以把原始字符串分成左右2部分,如下图 递归2.png 递归3.png 以中间的op为断点,分成左边S1和右边S2这2个字串,然后如果此时我们已经有字串的解了,记为List1和List2,那么我们要求的List就是分成遍历List1和List2,两两组合用中间的op计算结果。如果我们遍历所有的op,就遍历了所有expression拆成2个字串的可能性,再union起来,就是我们要求的答案。而怎么求出S1的List1,就是一个递归的过程,递归跳出的条件是,字串不再是num1 op num2的结构了,而是只包含一个数字,那么这个数字值就是这个字串对应的解。
递归的过程综合起来就是下图:
递归.png

带备忘录的递归

在递归思路的基础上,我们发现有很多计算是重复的。例如下图中的S1和上面图中的S1的计算结果,由于拆分的一级op不同,直接递归这部分会被计算2次,如果我们用一个备忘录把这个结果保存下来,计算字串的时候先去备忘录查找是否存在,就不用重复计算了。 递归拆2.png

Java版本代码
class Solution {

    private static Map<String, List<Integer>> map241 = new HashMap<>();
    
    public List<Integer> diffWaysToCompute(String expression) {
        if (map241.containsKey(expression)) {
            return map241.get(expression);
        }
        List<Integer> ans = new ArrayList<>();
        if (expression.length() == 0) {
            return ans;
        }
        int num = 0;
        int index = 0;
        for (index = 0; index < expression.length(); index++) {
            char c = expression.charAt(index);
            if (c >= '0' && c <= '9') {
                num = num * 10 + (c - '0');
            } else {
                break;
            }
        }
        if (index == expression.length()) {
            ans.add(num);
            map241.put(expression, ans);
            return ans;
        }
        for (int i = 1; i < expression.length()-1; i++) {
            char c = expression.charAt(i);
            if (isOperator241(c)) {
                List<Integer> left = diffWaysToCompute(expression.substring(0, i));
                List<Integer> right = diffWaysToCompute(expression.substring(i+1, expression.length()));
                for (int indexLeft = 0; indexLeft < left.size(); indexLeft++) {
                    for (int indexRight = 0; indexRight < right.size(); indexRight++) {
                        ans.add(caculate241(c, left.get(indexLeft), right.get(indexRight)));
                    }
                }
            }
        }
        map241.put(expression, ans);
        return ans;
    }

    /**
     * 判断是否是操作符
     * @param c
     * @return
     */
    private static boolean isOperator241(char c) {
        return c == '+' || c == '-' || c == '*';
    }

    /**
     * 操作符计算
     * @param operator
     * @param num1
     * @param num2
     * @return
     */
    private static int caculate241(char operator, int num1, int num2) {
        switch (operator) {
            case '+':
                return num1 + num2;
            case '-':
                return num1 - num2;
            case '*':
                return num1 * num2;
            default:
                break;
        }
        return 0;
    }
}

动态规划

有了上面带备忘录的递归基础,理解起动态规划的方法来相对简单。首先要对字符串预处理成上图的样子后,然后把num和op分别放入一个数组,记为numList和opList。

状态定义

定义二维数组dp,dp[i][j]的含义为,从下标为i的数字开始,到下标为j的数字结束(包含),可能的值列表。根据这个定义,我们要求的最终解就是dp[0][numLen-1]。

状态转移方程

这个状态转移方程比较特殊,不是简单的从后往前。我们了解了前面的递归过程,就会发现,dp[i][j]依赖的是,所有dp[i][k]和dp[k][j],即比dp[i][j]更短的dp值。所以状态转移方程可以表述为

dp[i][j] =union( dp[i][k] op dp[k][j] )
边界条件

对于不包含操作符的情况,值就只能等于数值本身

dp[0][0] = numList.get(0)
...
dp[numLen-1][numLen-1] = numList.get(numLen-1)
Java版本代码
class Solution {
    
    public List<Integer> diffWaysToCompute(String expression) {
        List<Integer> numList = new ArrayList<>();
        List<Character> operatorList = new ArrayList<>();
        int num = 0;
        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            if (isOperator241(c)) {
                numList.add(num);
                num = 0;
                operatorList.add(c);
            } else {
                num = num * 10 + (c - '0');
            }
        }
        // 因为表达式是合法的,所以肯定以数字结尾,上述遍历后,还需要把最后一个数字添加到数组中
        numList.add(num);
        int numSize = numList.size();
        ArrayList<Integer>[][] dp = (ArrayList<Integer>[][]) new ArrayList[numSize][numSize];
        for (int i = 0; i < numSize; i++) {
            ArrayList<Integer> list = new ArrayList<>();
            list.add(numList.get(i));
            dp[i][i] = list;
        }
        for (int len = 2; len <= numSize; len++) {
            for (int i = 0; i < numSize - len + 1; i++) {
                int j = i + len - 1;
                ArrayList<Integer> ans = new ArrayList<>();
                for (int mid = i; mid < j; mid++) {
                    List<Integer> left = dp[i][mid];
                    List<Integer> right = dp[mid+1][j];
                    for (int indexLeft = 0; indexLeft < left.size(); indexLeft++) {
                        for (int indexRight = 0; indexRight < right.size(); indexRight++) {
                            ans.add(caculate241(operatorList.get(mid), left.get(indexLeft), right.get(indexRight)));
                        }
                    }
                }
                dp[i][j] = ans;
            }
        }
        return dp[0][numSize-1];
    }

    /**
     * 判断是否是操作符
     * @param c
     * @return
     */
    private static boolean isOperator241(char c) {
        return c == '+' || c == '-' || c == '*';
    }

    /**
     * 操作符计算
     * @param operator
     * @param num1
     * @param num2
     * @return
     */
    private static int caculate241(char operator, int num1, int num2) {
        switch (operator) {
            case '+':
                return num1 + num2;
            case '-':
                return num1 - num2;
            case '*':
                return num1 * num2;
            default:
                break;
        }
        return 0;
    }
}