|
| 1 | +# 题目描述(困难难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +在数字中间添加加号、减号或者乘号,使得计算结果等于 `target`,返回所有的情况。 |
| 6 | + |
| 7 | +# 解法一 回溯法 |
| 8 | + |
| 9 | +自己开始想到了回溯法,但思路偏了,只能解决添加加号和减号,乘号不知道怎么处理。讲一下别人的思路,参考 [这里](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear)。 |
| 10 | + |
| 11 | +思路就是添加一个符号,再添加一个数字,计算到目前为止表达式的结果。 |
| 12 | + |
| 13 | +当到达字符串末尾的时候,判断当前结果是否等于 `target` 。 |
| 14 | + |
| 15 | +我们先考虑只有加法和减法,举个例子。 |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +选取第一个数字的时候不加符号,然后剩下的都是符号加数字的形式。 |
| 20 | + |
| 21 | +如果我们对上边的图做深度优先遍历,会发现我们考虑了所有的表达式,下边就是深度优先遍历的结果。 |
| 22 | + |
| 23 | +```java |
| 24 | +1 +2 +3 = 6 |
| 25 | +1 +2 -3 = 0 |
| 26 | +1 -2 +3 = 2 |
| 27 | +1 -2 -3 = -4 |
| 28 | +1 +23 = 24 |
| 29 | +1 -23 = -22 |
| 30 | +12 +3 = 15 |
| 31 | +12 -3 = 9 |
| 32 | +123 = 123 |
| 33 | +``` |
| 34 | + |
| 35 | +看一下只考虑加法和减法的代码。 |
| 36 | + |
| 37 | +有些地方需要注意,我们将字符串转成数字的时候可能会超出 `int` 范围,以及计算目前为止表达式的结果,也就是上图的括号里的值,也可能超出 `int` 范围,所以这两个值我们采用 `long` 。 |
| 38 | + |
| 39 | +还有就是 "01" 这种以 `0` 开头的字符串我们不用考虑,可以直接跳过。 |
| 40 | + |
| 41 | +计算上图括号中的值的时候,我们只需要用它的父节点中括号的值,加上或者减去当前值。 |
| 42 | + |
| 43 | +```java |
| 44 | +public List<String> addOperators2(String num, int target) { |
| 45 | + List<String> result = new ArrayList<>(); |
| 46 | + addOperatorsHelper(num, target, result, "", 0, 0, 0); |
| 47 | + return result; |
| 48 | +} |
| 49 | + |
| 50 | +// path: 目前为止的表达式 |
| 51 | +// 字符串开始的位置 |
| 52 | +// eval 目前为止计算的结果 |
| 53 | +private void addOperatorsHelper(String num, int target, List<String> result, String path, int start, long eval) { |
| 54 | + if (start == num.length()) { |
| 55 | + if (target == eval) { |
| 56 | + result.add(path); |
| 57 | + } |
| 58 | + return; |
| 59 | + |
| 60 | + } |
| 61 | + for (int i = start; i < num.length(); i++) { |
| 62 | + // 数字不能以 0 开头 |
| 63 | + if (num.charAt(start) == '0' && i > start) { |
| 64 | + break; |
| 65 | + } |
| 66 | + long cur = Long.parseLong(num.substring(start, i + 1)); |
| 67 | + // 选取第一个数不加符号 |
| 68 | + if (start == 0) { |
| 69 | + addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); |
| 70 | + } else { |
| 71 | + // 加当前值 |
| 72 | + addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); |
| 73 | + // 减当前值 |
| 74 | + addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); |
| 75 | + } |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +现在考虑乘法有什么不同。 |
| 81 | + |
| 82 | +还是以 `123` 为例。如果还是按照上边的代码的逻辑,如果添加乘法的话就是下边的样子。 |
| 83 | + |
| 84 | +```java |
| 85 | + 1 |
| 86 | + / |
| 87 | + +2(3) |
| 88 | + / |
| 89 | + *3(9) |
| 90 | +``` |
| 91 | + |
| 92 | +也就是 `1 +2 *3 = 9`,很明显是错的,原因就在于乘法的优先级较高,并不能直接将前边的结果乘上当前的数。而是用当前数乘以前一个操作数。 |
| 93 | + |
| 94 | +算的话,我们可以用之前的值(3)减去前一个操作数(2),然后再加上当前数(3)乘以前一个操作数(2)。 |
| 95 | + |
| 96 | +即,`3 - 2 + 3 * 2 = 7` |
| 97 | + |
| 98 | +所以代码的话,我们需要添加一个 `pre` 参数,用来记录上一个操作数。 |
| 99 | + |
| 100 | +对于加法和乘法,上一个操作数我们根据符号直接返回 `-2`,`3` 这种就可以了,但对于乘法有些不同。 |
| 101 | + |
| 102 | +如果是连乘的情况,比如对于 `2345`,假设之前已经进行到了 `2 +3 *4 (14)`,现在到 `5` 了。 |
| 103 | + |
| 104 | +如果我们想增加乘号,计算表达式 `2 +3 *4 *5` 的值。`5` 的前一个操作数是 `*4` ,我们应该记录的值是 `3 * 4 = 12` 。这样的话才能套用上边的讲的公式。 |
| 105 | + |
| 106 | +用之前的值(14)减去前一个操作数(12),然后再加上当前数(5)乘以前一个操作数(12)。 |
| 107 | + |
| 108 | +即,`14 - 12 + 5 * 12 = 62`。也就是 `2 +3 *4 *5 = 62`。 |
| 109 | + |
| 110 | +可以结合代码再看一下。 |
| 111 | + |
| 112 | +```java |
| 113 | +public List<String> addOperators(String num, int target) { |
| 114 | + List<String> result = new ArrayList<>(); |
| 115 | + addOperatorsHelper(num, target, result, "", 0, 0, 0); |
| 116 | + return result; |
| 117 | +} |
| 118 | + |
| 119 | +private void addOperatorsHelper(String num, int target, List<String> result, String path, int start, long eval, long pre) { |
| 120 | + if (start == num.length()) { |
| 121 | + if (target == eval) { |
| 122 | + result.add(path); |
| 123 | + } |
| 124 | + return; |
| 125 | + |
| 126 | + } |
| 127 | + for (int i = start; i < num.length(); i++) { |
| 128 | + // 数字不能以 0 开头 |
| 129 | + if (num.charAt(start) == '0' && i > start) { |
| 130 | + break; |
| 131 | + } |
| 132 | + long cur = Long.parseLong(num.substring(start, i + 1)); |
| 133 | + if (start == 0) { |
| 134 | + addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); |
| 135 | + } else { |
| 136 | + // 加当前值 |
| 137 | + addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); |
| 138 | + // 减当前值 |
| 139 | + addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); |
| 140 | + |
| 141 | + //乘法有两点不同 |
| 142 | + |
| 143 | + //当前表达式的值就是 先减去之前的值,然后加上 当前值和前边的操作数相乘 |
| 144 | + //eval - pre + pre * cur |
| 145 | + |
| 146 | + //另外 addOperatorsHelper 函数传进 pre 参数需要是 pre * cur |
| 147 | + //比如前边讲的 2+ 3 * 4 * 5 这种连乘的情况 |
| 148 | + addOperatorsHelper(num, target, result, path + "*" + cur, i + 1, eval - pre + pre * cur, pre * cur); |
| 149 | + } |
| 150 | + } |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +上边的我们 `path` 参数采用的是 `String` 类型,`String` 类型进行相加的话会生成新的对象,比较慢。 |
| 155 | + |
| 156 | +我们可以用 `StringBuilder` 类型。如果用 `StringBuilder` 的话,每次调用完函数就需要将之前添加的东西删除掉,然后再调用新的函数。 |
| 157 | + |
| 158 | +[评论区](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear) 介绍了 `StringBuilder` 删除之前添加的元素的方法,可以通过设置 `StringBuilder` 的长度。 |
| 159 | + |
| 160 | +```java |
| 161 | +public List<String> addOperators(String num, int target) { |
| 162 | + List<String> result = new ArrayList<>(); |
| 163 | + addOperatorsHelper(num, target, result, new StringBuilder(), 0, 0, 0); |
| 164 | + return result; |
| 165 | +} |
| 166 | + |
| 167 | +private void addOperatorsHelper(String num, int target, List<String> result, StringBuilder path, int start, long eval, long pre) { |
| 168 | + if (start == num.length()) { |
| 169 | + if (target == eval) { |
| 170 | + result.add(path.toString()); |
| 171 | + } |
| 172 | + return; |
| 173 | + |
| 174 | + } |
| 175 | + for (int i = start; i < num.length(); i++) { |
| 176 | + // 数字不能以 0 开头 |
| 177 | + if (num.charAt(start) == '0' && i > start) { |
| 178 | + break; |
| 179 | + } |
| 180 | + long cur = Long.parseLong(num.substring(start, i + 1)); |
| 181 | + int len = path.length(); |
| 182 | + if (start == 0) { |
| 183 | + addOperatorsHelper(num, target, result, path.append(cur), i + 1, cur, cur); |
| 184 | + path.setLength(len); |
| 185 | + } else { |
| 186 | + |
| 187 | + // 加当前值 |
| 188 | + addOperatorsHelper(num, target, result, path.append("+").append(cur), i + 1, eval + cur, cur); |
| 189 | + path.setLength(len); |
| 190 | + // 减当前值 |
| 191 | + addOperatorsHelper(num, target, result, path.append("-").append(cur), i + 1, eval - cur, -cur); |
| 192 | + path.setLength(len); |
| 193 | + // 乘当前值 |
| 194 | + addOperatorsHelper(num, target, result, path.append("*").append(cur), i + 1, eval - pre + pre * cur, |
| 195 | + pre * cur); |
| 196 | + path.setLength(len); |
| 197 | + } |
| 198 | + } |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +# 总 |
| 203 | + |
| 204 | +如果思路对了,用回溯法可以很快写出来,乘法的情况需要单独考虑一下。 |
| 205 | + |
| 206 | +我开始的思路是加数字再加符号,和上边的解法刚好是反过来了,但我的思路解决不了乘法的问题。 |
| 207 | + |
| 208 | +这里继续吸取教训,走到死胡同的时候,试着转变思路。 |
| 209 | + |
0 commit comments