[回溯 字符串] 282. 给表达式添加运算符(回溯法:考虑乘法优先级)
282. 给表达式添加运算符
题目链接:https://leetcode-cn.com/problems/expression-add-operators/
分类:
- 回溯法(画出回溯树、for循环划分数字、先实现加减法再实现乘法、乘法引入pre变量指向前一个因子)
- 字符串(递归传递字符串String和StringBuilder、保存现场和恢复现场)
思路:回溯法
本题的难点在于乘号的处理,乘法比加减法的优先级高,需要做特殊处理。而加减法的优先级是相同的,不需要做区分处理,所以我们可以先实现向表达式添加加减号,在此基础上实现添加乘号的功能。
先只考虑加减法的情况:
例如[123]为例画出回溯树:
回溯树的特点:同一层的兄弟节点就是同一层递归的不同轮for循环(因为我们用for循环来划分数字,所以同一层兄弟节点就是不同的数字划分情况,例如上图的第二层对剩余的23有两种划分:2,3和23),不同层的节点就是不同层递归,边代表加减号。
如果选择1作为一个因子,则它可以执行:
1+2,1-2,1+23,1-23
所以我们需要先提取一个数字作为因子,因为数字的划分位数不限,所以可以用for循环下标的移动来提取不同位数的数字,每一层递归划分数字的起点start是基于上一层递归所划分数字的右边界下标的下一位:
for(int i = start; i < len; i++)
long cur = Long.parseLong(str.substring(start, i + 1));//截取str[start,i]转换成数字
得到当前层的数字cur,就和之前的计算结果sum(上一层递归传递来的)分别做加法和减法,同时将加号"+“或减号”-"和cur加入到维护的表达式字符串path中,然后将计算结果sum和字符串path传递给下一次递归。
以此类推…
直到到达num的末尾,判断当前的计算结果sum是否等于target:
- 如果sum == target,就将path加入到解集;
- 如果sum != target,直接返回。
实现代码:(只考虑加减号)
//只考虑加减法的情况
class Solution {
List<String> res = new ArrayList<>();
public List<String> addOperators(String num, int target) {
String path = new String();
backtrack(num, target, 0, 0, path);
return res;
}
//回溯法:start指示这一层递归可以截取数字的起点
public void backtrack(String num, int target, long sum, int start, String path){
if(start == num.length() && sum == target){
res.add(new String(path));
return;
}
else if(start >= num.length()) return;
else{
for(int i = start; i < num.length(); i++){
//获取当前递归层的数字:可能溢出,所以转成long型
long cur = Long.parseLong(num.substring(start, i + 1));
//如果是第一个数字,则不需要加运算符
if(i == 0){
backtrack(num, target, sum + cur, i + 1, path + cur);
}
else{
//做 + 加法
backtrack(num, target, sum + cur, i + 1, path + "+" + cur);
//做 - 减法
backtrack(num, target, sum - cur, i + 1, path + "-" + cur);
}
}
}
}
}
接下来把乘法也考虑在内:
乘法会带来一个问题:
例如:[123],计算1+2*3
如果乘号和其他符号做同样的处理,则会计算成:1+2=3,3*3=9,计算错误。
正确做法:在处理*3时sum=3,cur=3,在计算乘法前,可以先把sum-2=1,再加上2*cur,即1+2*3=7计算正确。
因此,需要增加一个变量pre来记录上个因子,例如计算*3时,上个因子pre=2,乘法计算为:sum-pre+pre*cur
如何传递pre变量?
pre的传递根据是不是处理第一个数字,处理的是加、减、乘法都有不同的传递值。
例如:[123]
-
如果因子是第一个数字,要求1*2时,在计算*2时根据计算公式 sum-pre+pre*cur,取pre=1并不影响计算结果,所以在第一个数字时传递给下一层的pre就取cur;(第一个因子不可能是负数,因为"-"只作为二元运算符)
-
如果这一层递归所做的是加减法,则pre都是带正负号传递的,例如在计算-3时,3前面的-号就认为是3的正负号,把-3传递给下一层。
- 如果做的是加法,传递pre=cur;
- 如果做的是减法,传递pre=-cur;
-
如果这一层递归所做的是乘法,则pre的传递有所不同,需要考虑连乘的情况:
例如:1234,计算1+2*3*4,计算*4时,sum=7,cur=4,如果pre按加减法的规则传递,则这里pre=3,根据公式计算得:sum-pre+pre*cur=7-3+3*4=16计算错误。
正确的计算过程:计算*4时,sum=7,需要先将7-6=1,再计算1+4*6=25,所以此时的pre应该是2*3,所以如果做的是乘法,传递的pre=pre*cur,例如在计算*3时cur=3,pre=2,传递给下一层的pre=2*3=cur*pre.
实现遇到的问题:前导零问题
例如:[105],target=5,
输出:["1*0+5","1*5","+10-5","*10+5"]
预期结果:["1*0+5","10-5"]
原因分析:在字符串上划分数字时,例如对于105,不能划分为1和05,05不能看做5来参与计算,因为最终生成的字符串会变成1*5,丢失了0字符。
所以我们在一趟回溯过程中如果遇到“提取的数字以0开头且数字不止0这一位”,则直接退出这一趟回溯过程。
直接break跳出截取数字的for循环,而不是continue,因为continue是取i++进入下一轮for循环,但下一轮for循环截取数字的起点start还是不变的,如果此时start指向0,则后面所有截取的数字[start, i]都是以0开头的,所以直接break。
//解决前导零问题:遇到数字以0开头但不单只有0的情况直接退出当前for循环
if(num.charAt(start) == '0' && i > start) break;
实现代码(考虑加法、减法、乘法,表达式字符串使用String)
//考虑加减法、乘法的情况
class Solution {
List<String> res = new ArrayList<>();
public List<String> addOperators(String num, int target) {
backtrack(num, target, 0, 0, new String(), 0);
return res;
}
//回溯法:start指示这一层递归可以截取数字的起点
public void backtrack(String num, int target, long sum, int start, Stringpath, long pre){
if(start == num.length() && sum == target){
res.add(new String(path));
return;
}
else if(start >= num.length()) return;
else{
for(int i = start; i < num.length(); i++){
//解决前导零问题:遇到0开头的情况直接退出当前for循环
if(num.charAt(start) == '0' && i > start) break;
//获取当前递归层的数字:可能溢出,所以转成long型
long cur = Long.parseLong(num.substring(start, i + 1));
//如果cur是第一个数字,则不需要加运算符
if(start == 0){
backtrack(num, target, sum + cur, i + 1, path + cur, cur);
}
else{
//做加法 +cur
backtrack(num, target, sum + cur, i + 1, path + "+" + cur, cur);
//做减法 -cur
backtrack(num, target, sum - cur, i + 1, path + "-" + cur, -cur);
//做乘法 *cur
backtrack(num, target, sum-pre+pre*cur, i+1, path + "*" + cur, pre*cur);
}
}
}
}
}
代码优化:String → StringBuilder
使用String做组合拆分会带来很多空间上的浪费,我们使用StringBuilder来代替String。
StringBuilder保存现场和恢复现场(添加字符串和删除字符串)的方法:
- 添加字符串:可以和String一样,在调用递归函数时才在传参位置做append赋值.
- 删除字符串:
- 方法1:记录修改前的长度 + 手动delete
在修改sb前先记录下修改前的长度preLen,递归函数调用完需要恢复现场时,sb.delete(preLen,sb.length()),就可以将递归加入的部分删除。 - 方法2:记录修改前的长度 + 调整长度(效率最高,见setLength源码分析)
在修改sb前先记录下修改前的长度preLen,递归函数调用完需要恢复现场时,sb.setLength(preLen),通过缩小sb底层数组的长度来达到删除的效果。
- 方法1:记录修改前的长度 + 手动delete
实现代码:(考虑加法、减法、乘法,表达式字符串使用StringBuilder)
//考虑加减法、乘法的情况
class Solution {
List<String> res = new ArrayList<>();
public List<String> addOperators(String num, int target) {
// String path = new String();
backtrack(num, target, 0, 0, new StringBuilder(), 0);
return res;
}
//回溯法:start指示这一层递归可以截取数字的起点
public void backtrack(String num, int target, long sum, int start, StringBuilder path, long pre){
if(start == num.length() && sum == target){
res.add(new String(path));
return;
}
else if(start >= num.length()) return;
else{
for(int i = start; i < num.length(); i++){
//解决前导零问题:遇到0开头的情况直接退出当前for循环
if(num.charAt(start) == '0' && i > start) break;
//获取当前递归层的数字:可能溢出,所以转成long型
long cur = Long.parseLong(num.substring(start, i + 1));
//记录修改前的path长度
int preLen = path.length();
//如果cur是第一个数字,则不需要加运算符
if(start == 0){
// backtrack(num, target, sum + cur, i + 1, path + cur, cur);
backtrack(num, target, sum + cur, i + 1, path.append(cur), cur);
//path.delete(preLen, path.length());
path.setLength(preLen);
}
else{
//做加法 +cur
//backtrack(num, target, sum + cur, i + 1, path + "+" + cur, cur);
backtrack(num, target, sum + cur, i + 1, path.append("+").append(cur), cur);
// path.delete(preLen, path.length());
path.setLength(preLen);
//做减法 -cur
// backtrack(num, target, sum - cur, i + 1, path + "-" + cur, -cur);
backtrack(num, target, sum - cur, i + 1, path.append("-").append(cur), -cur);
//path.delete(preLen, path.length());
path.setLength(preLen);
//做乘法 *cur
//backtrack(num, target, sum-pre+pre*cur, i+1, path + "*" + cur, pre*cur);
backtrack(num, target, sum-pre+pre*cur, i+1, path.append("*").append(cur), pre*cur);
//path.delete(preLen, path.length());
path.setLength(preLen);
}
}
}
}
}