282. Expression Add Operators

Given a string num that contains only digits and an integer target, return all possibilities to insert the binary operators ‘+’, ‘-‘, and/or ‘*’ between the digits of num so that the resultant expression evaluates to the target value.

Note that operands in the returned expressions should not contain leading zeros.

Example 1:

Input: num = “123”, target = 6
Output: [“1*2*3“,”1+2+3“]
Explanation: Both “1_2_3” and “1+2+3” evaluate to 6.

Example 2:

Input: num = “232”, target = 8
Output: [“2*3+2“,”2+3*2“]
Explanation: Both “2*3+2“ and “2+3*2“ evaluate to 8.

Example 3:

Input: num = “3456237490”, target = 9191
Output: []
Explanation: There are no expressions that can be created from “3456237490” to evaluate to 9191.

回溯思路

设字符串 回溯算法与表达式计算 - 图1 的长度为 n,为构建表达式,我们可以往 回溯算法与表达式计算 - 图2 中间的 回溯算法与表达式计算 - 图3 个空隙添加 回溯算法与表达式计算 - 图4 号、回溯算法与表达式计算 - 图5 号或 回溯算法与表达式计算 - 图6号,或者不添加符号。

我们可以用「回溯法」来模拟这个过程。从左向右构建表达式,并实时计算表达式的结果。由于乘法运算优先级高于加法和减法运算,我们还需要保存最后一个连乘串(如 回溯算法与表达式计算 - 图7)的运算结果。

这里困难的是如何处理优先级运算回溯算法与表达式计算 - 图8。我们可以每次记录当前结果回溯算法与表达式计算 - 图9和最后计入(回溯算法与表达式计算 - 图10) 回溯算法与表达式计算 - 图11的连乘串回溯算法与表达式计算 - 图12。这样万一下面还是回溯算法与表达式计算 - 图13号,则撤销上一步回溯算法与表达式计算 - 图14计算结果,重新加上回溯算法与表达式计算 - 图15的结果作为回溯算法与表达式计算 - 图16,从而达到直接按从左到右的顺序求出有乘法的表达式。

定义递归函数 回溯算法与表达式计算 - 图17#card=math&code=%5Ctextit%7Bbacktrack%7D%28%5Ctextit%7Bexpr%7D%2C%20i%2C%20%5Ctextit%7Bres%7D%2C%20%5Ctextit%7Bmul%7D%29&id=nhTgr),其中:

  • 回溯算法与表达式计算 - 图18 为当前构建出的表达式;
  • 回溯算法与表达式计算 - 图19 表示当前的枚举到了 回溯算法与表达式计算 - 图20 的第 $i $个数字;
  • 回溯算法与表达式计算 - 图21为当前表达式的计算结果;
  • 回溯算法与表达式计算 - 图22 为表达式最后一个连乘串的计算结果。

该递归函数分为两种情况:

如果 回溯算法与表达式计算 - 图23,说明表达式已经构造完成,若此时有 回溯算法与表达式计算 - 图24,则找到了一个可行解,我们将 回溯算法与表达式计算 - 图25放入答案数组中,递归结束;

如果 回溯算法与表达式计算 - 图26,需要枚举当前表达式末尾要添加的符号(回溯算法与表达式计算 - 图27 号、回溯算法与表达式计算 - 图28 号或 回溯算法与表达式计算 - 图29 号),以及该符号之后需要截取多少位数字。设该符号之后的数字为 回溯算法与表达式计算 - 图30,按符号分类讨论:

  • 若添加 回溯算法与表达式计算 - 图31 号,则 回溯算法与表达式计算 - 图32 增加 回溯算法与表达式计算 - 图33,且 回溯算法与表达式计算 - 图34 单独组成表达式最后一个连乘串;
  • 若添加 回溯算法与表达式计算 - 图35 号,则 回溯算法与表达式计算 - 图36 减少 回溯算法与表达式计算 - 图37,且回溯算法与表达式计算 - 图38 单独组成表达式最后一个连乘串;
  • 若添加 回溯算法与表达式计算 - 图39号,由于乘法运算优先级高于加法和减法运算,我们需要对 回溯算法与表达式计算 - 图40 撤销之前 回溯算法与表达式计算 - 图41的计算结果,并添加新的连乘结果 回溯算法与表达式计算 - 图42,也就是将 回溯算法与表达式计算 - 图43减少 回溯算法与表达式计算 - 图44并增加回溯算法与表达式计算 - 图45

代码实现时,为避免字符串拼接所带来的额外时间开销,我们采用字符数组的形式来构建表达式。此外,运算过程中可能会产生超过 32 位整数的结果,我们要用 64 位整数存储中间运算结果

代码

  1. class Solution {
  2. public:
  3. vector<string> addOperators(string num, int target) {
  4. int n = num.length();
  5. vector<string> ans;
  6. function<void(string&, int, long, long)> backtrack = [&](string &expr, int i, long res, long mul) {
  7. if (i == n) {
  8. if (res == target) {
  9. ans.emplace_back(expr);
  10. }
  11. return;
  12. }
  13. int signIndex = expr.size();
  14. if (i > 0) { //刚开始不用占位!!
  15. expr.push_back('T'); // 占位符,下面填充具体符号
  16. }
  17. long val = 0;
  18. // 枚举截取的数字长度(取多少位),注意数字可以是单个 0 但不能有前导零
  19. for (int j = i; j < n && (j == i || num[i] != '0'); ++j) {
  20. val = val * 10 + num[j] - '0';
  21. expr.push_back(num[j]);
  22. if (i == 0) { // 表达式开头不能添加符号
  23. backtrack(expr, j + 1, val, val);
  24. } else { // 枚举符号
  25. expr[signIndex] = '+'; backtrack(expr, j + 1, res + val, val);
  26. expr[signIndex] = '-'; backtrack(expr, j + 1, res - val, -val);
  27. expr[signIndex] = '*'; backtrack(expr, j + 1, res - mul + mul * val, mul * val);
  28. }
  29. }
  30. expr.resize(signIndex);//注意这里,退出递归后要恢复当前表达式。撤销递归后选择。
  31. };
  32. string expr;
  33. backtrack(expr, 0, 0, 0);
  34. return ans;
  35. }
  36. };

这里处理乘法很麻烦,关键在于backtrack(expr,i+1,res-mul+mul*curr,mul*curr); 这句,对于同一个式子,计算表达式的结果,和最后一个乘积的值,可以直接按从左到右的顺序求出有乘法的表达式。

注意:

  1. 这里撤销选择的方式,可以理解为如下:
// 枚举符号
expr[signIndex] = '+'; 
backtrack(expr, j + 1, res + val, val);
expr[signIndex] = '-'; //更换表达式expr op val中的op字段
backtrack(expr, j + 1, res - val, -val);
expr[signIndex] = '*'; //更换表达式expr op val中的op字段,
backtrack(expr, j + 1, res - mul + mul * val, mul * val);
expr.resize(signIndex);//撤销所有选择,可以考虑连续两次递归
  1. 这里j == i || num[i] != '0'的意思是如果当前位'0',那么循环只到当前结束(j==i),否则可以继续。

复杂度分析

时间复杂度:回溯算法与表达式计算 - 图46#card=math&code=O%284%5En%29&id=qnVFy),其中 n 是字符串 回溯算法与表达式计算 - 图47 的长度。由于在数字之间可以选择不添加符号、添加 回溯算法与表达式计算 - 图48 号、回溯算法与表达式计算 - 图49 号或回溯算法与表达式计算 - 图50号,一共有 4 种选择,因此时间复杂度为 回溯算法与表达式计算 - 图51#card=math&code=O%284%5En%29&id=lWo53)。

注:考虑到将 回溯算法与表达式计算 - 图52 的拷贝存入答案需要花费 回溯算法与表达式计算 - 图53#card=math&code=O%28n%29&id=vRKPV)的时间,最终的时间复杂度似乎是 回溯算法与表达式计算 - 图54#card=math&code=O%28n%20%5Ctimes%204%5En%29&id=z1FYK)。果真如此吗?考虑合法表达式最多的情况,即 回溯算法与表达式计算 - 图55全为 回溯算法与表达式计算 - 图56,且 回溯算法与表达式计算 - 图57的情况,由于不能有前导零,我们必须在数字之间添加 回溯算法与表达式计算 - 图58三者之一,所以合法表达式有 回溯算法与表达式计算 - 图59个,因此「将 回溯算法与表达式计算 - 图60的拷贝存入答案」这一部分的时间开销至多为 回溯算法与表达式计算 - 图61#card=math&code=O%28n%20%5Ctimes%203%5En%29&id=y1YqJ)。

空间复杂度:回溯算法与表达式计算 - 图62#card=math&code=O%28n%29&id=LtUpY)。不考虑返回值的空间占用,空间复杂度取决于递归时的栈空间。