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
((23)-(45)) = -14
((2(3-4))5) = -10
(2((3-4)5)) = -10
(((23)-4)*5) = 10
思路分析
这题虽然归在“动态规划”的类别中,但是一开始也没想到怎么定义状态和写出状态转移方程。尝试了一下带备忘录的递归,也AC过了,后来看了其他人的题解,才对动态规划的方法豁然开朗,这里把2种思路都记录一下。
递归
在expression中,把数字记为num,把操作符记为op,那么expression就是数字和操作符交替出现的状态,例如
那么,任何一个操作符,都可以把原始字符串分成左右2部分,如下图
以中间的op为断点,分成左边S1和右边S2这2个字串,然后如果此时我们已经有字串的解了,记为List1和List2,那么我们要求的List就是分成遍历List1和List2,两两组合用中间的op计算结果。如果我们遍历所有的op,就遍历了所有expression拆成2个字串的可能性,再union起来,就是我们要求的答案。而怎么求出S1的List1,就是一个递归的过程,递归跳出的条件是,字串不再是num1 op num2的结构了,而是只包含一个数字,那么这个数字值就是这个字串对应的解。
递归的过程综合起来就是下图:
带备忘录的递归
在递归思路的基础上,我们发现有很多计算是重复的。例如下图中的S1和上面图中的S1的计算结果,由于拆分的一级op不同,直接递归这部分会被计算2次,如果我们用一个备忘录把这个结果保存下来,计算字串的时候先去备忘录查找是否存在,就不用重复计算了。
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;
}
}