306. 累加数
1、题干
累加数 是一个字符串,组成它的数字可以形成累加序列。
一个有效的 累加序列 必须 至少 包含 3 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。
给你一个只包含数字 '0'-'9'
的字符串,编写一个算法来判断给定输入是否是 累加数 。如果是,返回 true
;否则,返回 false
。
说明: 累加序列里的数,除数字 0 之外,不会 以 0 开头,所以不会出现 1, 2, 03
或者 1, 02, 3
的情况。
示例 1:
输入:
"112358"
输出: true
解释: 累加序列为:1, 1, 2, 3, 5, 8
。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
示例 2:
输入:
"199100199"
输出: true
解释: 累加序列为:1, 99, 100, 199。
1 + 99 = 100, 99 + 100 = 199
提示:
1 <= num.length <= 35
num
仅由数字(0
-9
)组成
进阶: 你计划如何处理由过大的整数输入导致的溢出?
2、解法1-回溯
总体思路:使用回溯对所有可能性进行匹配直到成功。大体步骤如下:
- 使用递归实现回溯算法,递归函数
match
,参数l
、m
、r
分别表示待匹配的3个数的起始位置 match
函数第一步先处理边界情况、0开头的特殊情况- 第二步从
num
字符串中取出前两个数并求和,得到第三个数n3
- 第三步剪枝,如果剩余字符比要查找的结果更短,直接返回false
- 第四步在剩余字符串中查找
n3
是否存在,若存在且正好用尽所有字符则返回true
- 若不存在则尝试
r
右移一位以及m
右移一位的情况
注意点
- 测试用例中不存在大整数,因此没有做大整数特殊处理
- 边界条件、特殊情况略多,如果不能AC可以多调试看看是否边界没处理好
- 一定要剪枝,否则大概率会超时
3、代码
var isAdditiveNumber = function (num) {
function match(l, m, r) {
// 处理边界情况、特殊情况(0开头的数)
if (r > num.length || m > num.length - 1 || (num[l] === '0' && m - l > 1)) return false;
if (m >= r) return match(l, m, r + 1);
if (num[m] === '0' && r - m > 1) return match(l, m + 1, r);
const n3 = `${+num.slice(l, m) + +num.slice(m, r)}`;
// 剪枝:如果剩余字符比要查找的结果更短,直接返回false
if (n3.length > num.length - r) return false;
// 匹配成功 🚀🚀🚀
if (num.slice(r, r + n3.length) === n3 && (r + n3.length === num.length || match(m, r, r + n3.length))) return true;
// 匹配失败:尝试查找其他可能性
return match(l, m, r + 1) || match(l, m + 1, r);
}
return match(0, 1, 2);
};
4、执行结果
执行用时: 108 ms 内存消耗: 44.7 MB
5、解法1-优化版
- 问题1:确定开头两个数字之后(
num.slice(r, r + n3.length) === n3
)的后续匹配方式存在无效遍历,原先使用递归进行后续匹配,每次右移1位,因此存在大量无效匹配。 - 问题2:已遍历过的情况没有直接跳过,导致存在大量重复遍历。
- 优化1:确定开头两个数字之后,由于后面的数字是前面累加而来,因此后面的数字都是确定的,可以直接使用
while
循环匹配累加和。 - 优化2:使用哈希表记录已遍历过的情况,后续直接跳过这些情况,进行记忆化搜索。(感谢评论区 @Jvaeyhcd 的提醒)
在用例
"1991000199299498797"
中遍历次数从解法1的15000+次降到99次。
6、代码
var isAdditiveNumber = function (num) {
const set = new Set();
function match(l, m, r) {
if (set.has(`${l}-${m}-${r}`)) return false;
set.add(`${l}-${m}-${r}`);
if (r > num.length || m > num.length - 1 || (num[l] === '0' && m - l > 1)) return false;
if (r - m > num.length - r || m - l > num.length - r) return false;
if (m >= r) return match(l, m, r + 1);
if (num[m] === '0' && r - m > 1) return match(l, m + 1, r);
let r2 = r, n1 = +num.slice(l, m), n2 = +num.slice(m, r), n3 = n1 + n2;
while (num.slice(r2, r2 + `${n3}`.length) === `${n3}`) {
(r2 = r2 + `${n3}`.length), (n1 = n2), (n2 = n3), (n3 = n1 + n2);
}
if (r2 === num.length) return true;
return match(l, m, r + 1) || match(l, m + 1, r);
}
return match(0, 1, 2);
};
7、执行结果
执行用时: 68 ms 内存消耗: 38.1 MB
8、解法2-双指针遍历
总体思路:双层遍历确定开头两个数字之后,由于后面的数字是前面累加而来,因此后面的数字都是确定的,可以直接使用while
循环匹配累加和。
在用例
"1991000199299498797"
中遍历次数从解法1的15000+次降到120次。
9、代码
var isAdditiveNumber = function (num) {
for (let m = 1; m < num.length; m++) {
let n1 = +num.slice(0, m);
if (num[0] === '0' && m > 1) return false;
for (let r = m + 1; r < num.length; r++) {
if (num[m] === '0' && r - m > 1) break;
let n2 = +num.slice(m, r), r_2 = r, n_1 = n1, n3 = n1 + n2;
while (num.slice(r_2, r_2 + `${n3}`.length) === `${n3}`) {
(r_2 = r_2 + `${n3}`.length), (n_1 = n2), (n2 = n3), (n3 = n_1 + n2);
}
if (r_2 === num.length) return true;
}
}
return false;
};
10、执行结果
执行用时: 60 ms 内存消耗: 38.1 MB