[回溯 字符串] 282. 给表达式添加运算符(回溯法:考虑乘法优先级)

10 篇文章 0 订阅
7 篇文章 0 订阅

[回溯 字符串] 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底层数组的长度来达到删除的效果。

实现代码:(考虑加法、减法、乘法,表达式字符串使用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);
                }
            }
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值