|
| 1 | +# 题目描述(中等难度) |
| 2 | + |
| 3 | +306、Additive Number |
| 4 | + |
| 5 | +Additive number is a string whose digits can form additive sequence. |
| 6 | + |
| 7 | +A valid additive sequence should contain **at least** three numbers. Except for the first two numbers, each subsequent number in the sequence must be the sum of the preceding two. |
| 8 | + |
| 9 | +Given a string containing only digits `'0'-'9'`, write a function to determine if it's an additive number. |
| 10 | + |
| 11 | +**Note:** Numbers in the additive sequence **cannot** have leading zeros, so sequence `1, 2, 03` or `1, 02, 3` is invalid. |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +**Example 1:** |
| 16 | + |
| 17 | +``` |
| 18 | +Input: "112358" |
| 19 | +Output: true |
| 20 | +Explanation: The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. |
| 21 | + 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 |
| 22 | +``` |
| 23 | + |
| 24 | +**Example 2:** |
| 25 | + |
| 26 | +``` |
| 27 | +Input: "199100199" |
| 28 | +Output: true |
| 29 | +Explanation: The additive sequence is: 1, 99, 100, 199. |
| 30 | + 1 + 99 = 100, 99 + 100 = 199 |
| 31 | +``` |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +**Constraints:** |
| 36 | + |
| 37 | +- `num` consists only of digits `'0'-'9'`. |
| 38 | +- `1 <= num.length <= 35` |
| 39 | + |
| 40 | +**Follow up:** |
| 41 | +How would you handle overflow for very large input integers? |
| 42 | + |
| 43 | +给一个字符串,判断字符串符不符合某种形式。就是从开头选两个数,它俩的和刚好是下一个数,然后第 `2` 个数和第 `3` 数字的和又是下一个数字,以此类推。 |
| 44 | + |
| 45 | +# 解法一 |
| 46 | + |
| 47 | +理解了题意的话很简单,可以勉强的归到回溯法的问题中,我们从暴力求解的角度考虑。 |
| 48 | + |
| 49 | +首先需要两个数,我们可以用两层 `for` 循环依次列举。 |
| 50 | + |
| 51 | +```javascript |
| 52 | +//i 表示第一个数字的结尾(不包括 i) |
| 53 | +for (let i = 1; i < num.length; i++) { |
| 54 | + //j 表示从 i 开始第二个数字的结尾(不包括 j) |
| 55 | + for (let j = i + 1; j < num.length; j++) { |
| 56 | + let num1 = Number(num.substring(0, i)); |
| 57 | + let num2 = Number(num.substring(i, j)); |
| 58 | + } |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +然后需要处理下特殊情况,将以 `0` 开头但不是`0`的数字去掉,比如 `023` 这种是不合法的, |
| 63 | + |
| 64 | +```java |
| 65 | +//i 表示第一个数字的结尾(不包括 i) |
| 66 | +for (let i = 1; i < num.length; i++) { |
| 67 | + // 0 开头, 并且当前数字不是 0 |
| 68 | + if (num[0] === '0' && i > 1) { |
| 69 | + return false; |
| 70 | + } |
| 71 | + //j 表示从 i 开始第二个数字的结尾(不包括 j) |
| 72 | + for (let j = i + 1; j < num.length; j++) { |
| 73 | + // 0 开头, 并且当前数字不是 0 |
| 74 | + if (num[i] === '0' && j - i > 1) { |
| 75 | + break; |
| 76 | + } |
| 77 | + let num1 = Number(num.substring(0, i)); |
| 78 | + let num2 = Number(num.substring(i, j)); |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +有了这两个数字,下边的就好说了。 |
| 84 | + |
| 85 | +只需要计算 `sum = num1 + num2` ,然后看 `sum` 在不在接下来的字符串中。 |
| 86 | + |
| 87 | +如果不在,那么就考虑下一个 `num1` 和 `num2` 。 |
| 88 | + |
| 89 | +如果在的话,`num1` 就更新为 `num2`,`num2` 更新为 `sum` ,有了新的 `num1`和 `num2` ,然后继续按照上边的步骤考虑。 |
| 90 | + |
| 91 | +举个例子, |
| 92 | + |
| 93 | +```java |
| 94 | +1 2 2 1 4 16 |
| 95 | + ^ ^ |
| 96 | + i j |
| 97 | + |
| 98 | +num1 = 12 |
| 99 | +num2 = 2 |
| 100 | +sum = num1 + num2 = 14 |
| 101 | + |
| 102 | +接下来的数字刚好是 14, 那么就更新 num1 和 num2 |
| 103 | + |
| 104 | +num1 = num2 = 2 |
| 105 | +num2 = sum = 14 |
| 106 | + |
| 107 | +然后继续判断即可。 |
| 108 | +``` |
| 109 | + |
| 110 | +代码的话,我们可以写成递归的形式。 |
| 111 | + |
| 112 | +```javascript |
| 113 | +/** |
| 114 | + * @param {string} num |
| 115 | + * @return {boolean} |
| 116 | + */ |
| 117 | +var isAdditiveNumber = function (num) { |
| 118 | + if (num.length === 0) { |
| 119 | + return true; |
| 120 | + } |
| 121 | + for (let i = 1; i < num.length; i++) { |
| 122 | + if (num[0] === '0' && i > 1) { |
| 123 | + return false; |
| 124 | + } |
| 125 | + for (let j = i + 1; j < num.length; j++) { |
| 126 | + if (num[i] === '0' && j - i > 1) { |
| 127 | + break; |
| 128 | + } |
| 129 | + let num1 = Number(num.substring(0, i)); |
| 130 | + let num2 = Number(num.substring(i, j)); |
| 131 | + if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { |
| 132 | + return true; |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + return false; |
| 137 | +}; |
| 138 | + |
| 139 | +function isAdditiveNumberHelper(num, num1, num2) { |
| 140 | + if (num.length === 0) { |
| 141 | + return true; |
| 142 | + } |
| 143 | + //依次列举数字,判断是否等于 num1 + num2 |
| 144 | + for (let i = 1; i <= num.length; i++) { |
| 145 | + //不考虑以 0 开头的数字 |
| 146 | + if (num[0] === '0' && i > 1) { |
| 147 | + return false; |
| 148 | + } |
| 149 | + let sum = Number(num.substring(0, i)); |
| 150 | + if (num1 + num2 === sum) { |
| 151 | + //传递剩下的字符串以及新的 num1 和 num2 |
| 152 | + return isAdditiveNumberHelper(num.substring(i), num2, sum); |
| 153 | + //此时大于了 num1 + num2, 再往后遍历只会更大, 所以直接结束 |
| 154 | + } else if (num1 + num2 < sum) { |
| 155 | + break; |
| 156 | + } |
| 157 | + } |
| 158 | + return false; |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +主要思想就是上边的了,看了 [这里](https://leetcode.com/problems/additive-number/discuss/75567/Java-Recursive-and-Iterative-Solutions) 的分析,代码的话,可以简单的优化下。 |
| 163 | + |
| 164 | +第一点,如果两个数的位数分别是 `a` 位和 `b` 位,`a >= b`,那么它俩相加得到的和至少是 `a` 位。比如 `100 + 99` 得到 `199`,依旧是三位数。 |
| 165 | + |
| 166 | +所以我们的 `num1` 和 `num2` 其中的一个数的位数一定不能大于等于字符串总长度的一半,不然的话剩下的字符串一定不等于 `num1` 和 `num2` 的和,因为剩下的长度不够了。 |
| 167 | + |
| 168 | +第二点,上边的代码中 `isAdditiveNumberHelper` 函数,略微写的复杂了些,我们可以直接利用 `String` 的 `startsWith` 函数,判断 `num1 + num2 ` 是不是字符串的开头即可。 |
| 169 | + |
| 170 | +基于上边两点,优化后的代码如下。 |
| 171 | + |
| 172 | +```javascript |
| 173 | +/** |
| 174 | + * @param {string} num |
| 175 | + * @return {boolean} |
| 176 | + */ |
| 177 | +var isAdditiveNumber = function (num) { |
| 178 | + if (num.length === 0) { |
| 179 | + return true; |
| 180 | + } |
| 181 | + //这里取了等号,是因为长度是奇数的时候,除以二是向下取整 |
| 182 | + for (let i = 1; i <= num.length / 2; i++) { |
| 183 | + if (num[0] === '0' && i > 1) { |
| 184 | + return false; |
| 185 | + } |
| 186 | + for (let j = i + 1; j < num.length; j++) { |
| 187 | + if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { |
| 188 | + break; |
| 189 | + } |
| 190 | + let num1 = Number(num.substring(0, i)); |
| 191 | + let num2 = Number(num.substring(i, j)); |
| 192 | + if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { |
| 193 | + return true; |
| 194 | + } |
| 195 | + } |
| 196 | + } |
| 197 | + return false; |
| 198 | +}; |
| 199 | + |
| 200 | +function isAdditiveNumberHelper(num, num1, num2) { |
| 201 | + if (num.length === 0) { |
| 202 | + return true; |
| 203 | + } |
| 204 | + return num.startsWith(num1 + num2) && isAdditiveNumberHelper(num.substring((num1+num2+'').length), num2, num1 + num2); |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +当然,我们最初的分析很明显是迭代的形式,我们内层也可以直接写成循环,不需要递归函数。 |
| 209 | + |
| 210 | +```javascript |
| 211 | +/** |
| 212 | + * @param {string} num |
| 213 | + * @return {boolean} |
| 214 | + */ |
| 215 | +var isAdditiveNumber = function (num) { |
| 216 | + if (num.length === 0) { |
| 217 | + return true; |
| 218 | + } |
| 219 | + for (let i = 1; i <= num.length / 2; i++) { |
| 220 | + if (num[0] === '0' && i > 1) { |
| 221 | + return false; |
| 222 | + } |
| 223 | + for (let j = i + 1; j < num.length; j++) { |
| 224 | + if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { |
| 225 | + break; |
| 226 | + } |
| 227 | + let num1 = Number(num.substring(0, i)); |
| 228 | + let num2 = Number(num.substring(i, j)); |
| 229 | + let temp = num.substring(j); |
| 230 | + while(temp.startsWith(num1 + num2)) { |
| 231 | + let n = num1; |
| 232 | + num1 = num2; |
| 233 | + num2 = num1 + n; |
| 234 | + temp = temp.substring((num2 + '').length); |
| 235 | + if (temp.length === 0) { |
| 236 | + return true; |
| 237 | + } |
| 238 | + } |
| 239 | + } |
| 240 | + } |
| 241 | + return false; |
| 242 | +}; |
| 243 | +``` |
| 244 | + |
| 245 | +还有一个扩展,`How would you handle overflow for very large input integers?` |
| 246 | + |
| 247 | +让我们考虑数字太大了怎么办,此时解决大数相加的问题即可,所有的操作在字符串层面上进行。参考 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html)。 |
| 248 | + |
| 249 | +# 总 |
| 250 | + |
| 251 | +这道题主要的想法就是列举所有情况,但总觉得先单独列举两个数字不够优雅,思考了下怎么把它合并到递归中,无果,悲伤。 |
| 252 | + |
| 253 | +上边其实是一种思路,只是在写法上可能有所不同,唯一的优化就是列举数字的时候考虑一半即可,但时间复杂度不会变化。 |
| 254 | + |
| 255 | +之所以说勉强归到回溯法,是因为遍历路径的感觉是,先选两个数,然后一路到底,失败的话不是回到上一层,而是直接回到开头,然后重新选取两个数,继续一路到底,不是典型的回溯。 |
0 commit comments