【分治】力扣241:为运算表达式设计优先级(好优雅的代码)

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

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

分治问题由 分 (divide)和 治 (conquer)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。归并排序就是典型的分治问题,其中【分】即为把大数组平均分成两个小数组,通过递归实现,最终得到多个长度为 1 的子数组;【治】即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为 1 的子数组开始,最终合成一个大数组。

方法1:分治
利用分治思想,可以把加括号转化为:对于每个运算符号,先执行处理两侧的数学表达式,再处理此运算符号。注意边界情况,即字符串内无运算符号,只有数字,那结果就是给定的数字的int类型。
三步走:

  1. 分解:按运算符分成左右两部分,分别求解
  2. 解决:实现一个递归函数,输入算式,返回算式解
  3. 合并:根据运算符合并左右两部分的解,得出最终解

以 2 * 3 - 4 * 5 为例:
2 和 3 - 4 * 5 两部分,中间是 * 号相连。
2 * 3 和 4 * 5 两部分,中间是 - 号相连。
2 * 3 - 4 和 5 两部分,中间是 * 号相连。
有了两部分的结果,然后再通过中间的符号两两计算加入到最终的结果中即可。
比如第一种情况,2 和 3 - 4 * 5 两部分,中间是 * 号相连。
2 的解就是 [2],3 - 4 * 5 的解就是 [-5, -17]。
把两部分解通过 * 号计算,最终结果就是 [-10, -34]。
另外两种情况也类似。
然后还需要递归出口。
如果给定的字符串只有数字,没有运算符,那结果就是给定的字符串转为数字。比如上边的第一种情况,2 的解就是 [2]。
image

class Solution:
    def diffWaysToCompute(self, expression: str) -> List[int]:
        # 先考虑边界情况
        if expression.isdigit():
            return [int(expression)]
        res = [] # 构造返回值,是一个list
        for i, op in enumerate(expression): # 变量i为字符串中的数字,op为运算符号
            if op in ['+', '-', '*']:
                # 1.分解 2.求解
                left = self.diffWaysToCompute(expression[: i])
                right = self.diffWaysToCompute(expression[i + 1 :])
                # 3.合并
                for l in left:
                    for r in right:
                        if op == '+':
                            res.append(l + r)
                        elif op == '-':
                            res.append(l - r)
                        else:
                            res.append(l * r)
        return res

抄一个简短一点的作业,用到了eval

class Solution:
    def diffWaysToCompute(self, s: str) -> List[int]:
        if s.isdigit(): return [int(s)]
        res = []
        for i, c in enumerate(s):
            if c in '+-*':
                for left in self.diffWaysToCompute(s[:i]):
                    for right in self.diffWaysToCompute(s[i+1:]):
                        res.append(eval(f'{left}{c}{right}'))
        return res

可以发现,某些被 divide 的子字符串可能重复出现多次,因此可以用 memoization(一种空间换时间的方法) 来去重,将递归过程中的解保存起来,如果第二次递归过来,直接返回结果即可,无需重复递归。

# Java

//添加一个 map 存储解,其中 key 存储函数入口参数的字符串,value 存储当前全部解的一个 List
HashMap<String,List<Integer>> map = new HashMap<>();
public List<Integer> diffWaysToCompute(String input) {
    if (input.length() == 0) {
        return new ArrayList<>();
    }
    //如果已经有当前解了,直接返回
    if(map.containsKey(input)){
        return map.get(input);
    }
    List<Integer> result = new ArrayList<>();
    int num = 0;
    int index = 0;
    while (index < input.length() && !isOperation(input.charAt(index))) {
        num = num * 10 + input.charAt(index) - '0';
        index++;
    }
    if (index == input.length()) {
        result.add(num);
        //存到 map
        map.put(input, result);
        return result;
    }
    for (int i = 0; i < input.length(); i++) {
        if (isOperation(input.charAt(i))) {
            List<Integer> result1 = diffWaysToCompute(input.substring(0, i));
            List<Integer> result2 = diffWaysToCompute(input.substring(i + 1));
            for (int j = 0; j < result1.size(); j++) {
                for (int k = 0; k < result2.size(); k++) {
                    char op = input.charAt(i);
                    result.add(caculate(result1.get(j), op, result2.get(k)));
                }
            }
        }
    }
     //存到 map
    map.put(input, result);
    return result;
}

private int caculate(int num1, char c, int num2) {
    switch (c) {
        case '+':
            return num1 + num2;
        case '-':
            return num1 - num2;
        case '*':
            return num1 * num2;
    }
    return -1;
}

private boolean isOperation(char c) {
    return c == '+' || c == '-' || c == '*';
}

作者:windliang
链接:https://leetcode-cn.com/problems/different-ways-to-add-parentheses/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-5-5/

方法2:与其从上到下用分治处理 + memoization,不如直接从下到上用动态规划处理。

  1. 最巧妙的地方就是做一个预处理,把每个数字提前转为 int 然后存起来,同时把运算符也都存起来。这样的话就有了两个 list,一个保存了所有数字,一个保存了所有运算符。

2 * 3 - 4 * 5
存起来的数字是 numList = [2 3 4 5],
存起来的运算符是 opList = [*, -, *]。

  1. dp[i][j] 也比较好定义了,含义是第 i 到第 j 个数字(从 0 开始计数)范围内的表达式的所有解。

2 * 3 - 4 * 5
dp[1][3] 就代表第一个数字 3 到第三个数字 5 范围内的表达式 3 - 4 * 5 的所有解。

  1. 初始条件的话,也很简单了,就是范围内只有一个数字。

2 * 3 - 4 * 5
dp[0][0] = [2],dp[1][1] = [3],dp[2][2] = [4],dp[3][3] = [5]。

有了一个数字的所有解,然后两个数字的所有解就可以求出来。
有了两个数字的所有解,然后三个数字的所有解就和解法一求法一样。
把三个数字分成两部分,将两部分的解两两组合起来即可。
两部分之间的运算符的话,因为表达式是一个数字一个运算符,所以运算符的下标就是左部分最后一个数字的下标。

假设求 dp[1][3]
也就是计算 3 - 4 * 5 的解
① 分成 3 和 4 * 5 两部分,3 对应的下标是 1 ,对应的运算符就是 opList[1] = '-' 。也就是计算 3 - 20 = -17
② 分成 3 - 4 和 5 两部分,4 的下标是 2 ,对应的运算符就是 opList[2] = '*'。也就是计算 -1 * 5 = -5
所以 dp[1][3] = [-17 -5]

四个、五个... 都可以分成两部分,然后通过之前的解求出来。
直到包含了所有数字的解求出来,假设数字总个数是 n,dp[0][n-1] 就是最后返回的了。

# Java

public List<Integer> diffWaysToCompute(String input) {
    List<Integer> numList = new ArrayList<>();
    List<Character> opList = new ArrayList<>();
    char[] array = input.toCharArray();
    int num = 0;
    for (int i = 0; i < array.length; i++) {
        if (isOperation(array[i])) {
            numList.add(num);
            num = 0;
            opList.add(array[i]);
            continue;
        }
        num = num * 10 + array[i] - '0';
    }
    numList.add(num);
    int N = numList.size(); // 数字的个数

    // 一个数字
    ArrayList<Integer>[][] dp = (ArrayList<Integer>[][]) new ArrayList[N][N];
    for (int i = 0; i < N; i++) {
        ArrayList<Integer> result = new ArrayList<>();
        result.add(numList.get(i));
        dp[i][i] = result;
    }
    // 2 个数字到 N 个数字
    for (int n = 2; n <= N; n++) {
        // 开始下标
        for (int i = 0; i < N; i++) {
            // 结束下标
            int j = i + n - 1;
            if (j >= N) {
                break;
            }
            ArrayList<Integer> result = new ArrayList<>();
            // 分成 i ~ s 和 s+1 ~ j 两部分
            for (int s = i; s < j; s++) {
                ArrayList<Integer> result1 = dp[i][s];
                ArrayList<Integer> result2 = dp[s + 1][j];
                for (int x = 0; x < result1.size(); x++) {
                    for (int y = 0; y < result2.size(); y++) {
                        // 第 s 个数字下标对应是第 s 个运算符
                        char op = opList.get(s);
                        result.add(caculate(result1.get(x), op, result2.get(y)));
                    }
                }
            }
            dp[i][j] = result;

        }
    }
    return dp[0][N-1];
}

private int caculate(int num1, char c, int num2) {
    switch (c) {
        case '+':
            return num1 + num2;
        case '-':
            return num1 - num2;
        case '*':
            return num1 * num2;
    }
    return -1;
}

private boolean isOperation(char c) {
    return c == '+' || c == '-' || c == '*';
}

作者:windliang
链接:https://leetcode-cn.com/problems/different-ways-to-add-parentheses/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-5-5/
posted @   Vonos  阅读(85)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示