【分治】力扣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类型。
三步走:
- 分解:按运算符分成左右两部分,分别求解
- 解决:实现一个递归函数,输入算式,返回算式解
- 合并:根据运算符合并左右两部分的解,得出最终解
以 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]。
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,不如直接从下到上用动态规划处理。
- 最巧妙的地方就是做一个预处理,把每个数字提前转为 int 然后存起来,同时把运算符也都存起来。这样的话就有了两个 list,一个保存了所有数字,一个保存了所有运算符。
2 * 3 - 4 * 5
存起来的数字是 numList = [2 3 4 5],
存起来的运算符是 opList = [*, -, *]。
- dp[i][j] 也比较好定义了,含义是第 i 到第 j 个数字(从 0 开始计数)范围内的表达式的所有解。
2 * 3 - 4 * 5
dp[1][3] 就代表第一个数字 3 到第三个数字 5 范围内的表达式 3 - 4 * 5 的所有解。
- 初始条件的话,也很简单了,就是范围内只有一个数字。
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/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构