目录

题目描述

306. 累加数

题意分析

输入是一个只含数字字符的字符串 num,要判断能否在它中间插入若干刀,把它切成至少三个数,使得从第三个数开始,每个数都恰好等于紧邻它前面两个数之和。切分必须是连续且不重不漏的,也就是说所有段拼起来必须还原成原串。

有两处约束信号必须抠住。第一,不允许非法前导零:合法的段要么是单个字符 0,要么首字符不为 0。所以 "1023" 不能切成 1, 0, 23 之外的形式去凑,"01" 这种段一律非法。第二,题面明确提示数字可能非常长,长到超出 64 位整数范围,这实际上是在告诉你不要走 parseLong 那条路。

最有价值的结构性观察是:这个序列由前两个数完全决定。一旦第一段和第二段定下来,第三段必须是它们的和,第四段必须是第二三段的和,整条链没有任何自由度。所以搜索空间不是「所有切分方式」的指数级,而只是「前两段的切点」这一对下标。

边界情况包括:串长小于 3 时不可能凑出三个数;单独的 "0""00""000" 要分别判断("000" 可以切成 0, 0, 0 是合法的,"00" 只有两段不合法);某一段的和可能比剩余字符串还长,此时应当直接判失败而不是越界访问。

解法:枚举前两段 + 字符串加法验证

核心思路

累加序列至少有三个数,且从第三个数开始,每一项都由前两项唯一确定。普通回溯会在每个位置枚举下一个切点;本题可以把回溯树压缩为:只枚举前两个数的结束位置,后续不断计算两数之和并匹配字符串前缀。

数字段可能超过 64 位整数范围,不能用 long 解析。用十进制字符串模拟加法:从末位向前相加,维护进位,最后反转结果。这样数值长度只受输入字符串限制。

前导零规则必须在枚举时处理:单独的 "0" 合法,但 "01" 非法。前两个数确定后,若其和不是剩余字符串的前缀,该分支立即失败;若能逐段恰好消费完整个字符串,则得到合法累加序列。

正确性来自两点:任意合法序列的前两个切点都会被双重循环枚举到;而这两个数一旦确定,后续没有其他选择,逐项匹配通过当且仅当整条序列满足累加关系。

解题步骤

  1. 枚举第一个数的结束位置;若它以 0 开头,长度只能为 1
  2. 枚举第二个数的结束位置,并应用同样的前导零规则。
  3. 若剩余长度小于两个加数的最大长度,连下一项都放不下,结束当前内层枚举。
  4. 用字符串加法算出下一项;检查它是否从当前下标开始精确匹配。
  5. 匹配后将两个加数向前滚动,直到字符串恰好消费完;任一位置不匹配则尝试下一组初始切点。

例如 "199100199" 选择前两项 "1""99" 后,依次得到并匹配 "100""199",最终恰好到达末尾,因此返回 true

代码实现

class Solution {
    public boolean isAdditiveNumber(String num) {
        int n = num.length();
        for (int firstEnd = 1; firstEnd < n - 1; firstEnd++) {
            if (num.charAt(0) == '0' && firstEnd > 1) {
                break;
            }

            for (int secondEnd = firstEnd + 1; secondEnd < n; secondEnd++) {
                if (num.charAt(firstEnd) == '0' && secondEnd - firstEnd > 1) {
                    break;
                }

                int maxAddendLength = Math.max(firstEnd, secondEnd - firstEnd);
                if (n - secondEnd < maxAddendLength) {
                    break;
                }

                String first = num.substring(0, firstEnd);
                String second = num.substring(firstEnd, secondEnd);
                if (matchesRest(num, secondEnd, first, second)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean matchesRest(String num, int index, String first, String second) {
        while (index < num.length()) {
            String sum = add(first, second);
            if (!num.startsWith(sum, index)) {
                return false;
            }
            index += sum.length();
            first = second;
            second = sum;
        }
        return true;
    }

    private String add(String first, String second) {
        StringBuilder reversed = new StringBuilder();
        int i = first.length() - 1;
        int j = second.length() - 1;
        int carry = 0;

        while (i >= 0 || j >= 0 || carry != 0) {
            int sum = carry;
            if (i >= 0) {
                sum += first.charAt(i--) - '0';
            }
            if (j >= 0) {
                sum += second.charAt(j--) - '0';
            }
            reversed.append(sum % 10);
            carry = sum / 10;
        }
        return reversed.reverse().toString();
    }
}
func isAdditiveNumber(num string) bool {
    n := len(num)
    for firstEnd := 1; firstEnd < n-1; firstEnd++ {
        if num[0] == '0' && firstEnd > 1 {
            break
        }

        for secondEnd := firstEnd + 1; secondEnd < n; secondEnd++ {
            if num[firstEnd] == '0' && secondEnd-firstEnd > 1 {
                break
            }

            maxAddendLength := firstEnd
            if secondEnd-firstEnd > maxAddendLength {
                maxAddendLength = secondEnd - firstEnd
            }
            if n-secondEnd < maxAddendLength {
                break
            }

            first := num[:firstEnd]
            second := num[firstEnd:secondEnd]
            if matchesAdditiveRest(num, secondEnd, first, second) {
                return true
            }
        }
    }
    return false
}

func matchesAdditiveRest(num string, index int, first string, second string) bool {
    for index < len(num) {
        sum := addDecimalStrings(first, second)
        if len(num)-index < len(sum) || num[index:index+len(sum)] != sum {
            return false
        }
        index += len(sum)
        first, second = second, sum
    }
    return true
}

func addDecimalStrings(first string, second string) string {
    reversed := make([]byte, 0, len(first)+len(second))
    i, j, carry := len(first)-1, len(second)-1, 0

    for i >= 0 || j >= 0 || carry != 0 {
        sum := carry
        if i >= 0 {
            sum += int(first[i] - '0')
            i--
        }
        if j >= 0 {
            sum += int(second[j] - '0')
            j--
        }
        reversed = append(reversed, byte(sum%10)+'0')
        carry = sum / 10
    }

    for left, right := 0, len(reversed)-1; left < right; left, right = left+1, right-1 {
        reversed[left], reversed[right] = reversed[right], reversed[left]
    }
    return string(reversed)
}

复杂度分析

设字符串长度为 n

  • 时间复杂度:$O(n^3)$。前两个切点有 $O(n^2)$ 种组合,每组最多用 $O(n)$ 的字符运算验证后续序列。
  • 空间复杂度:$O(n)$。字符串加法产生的结果和缓冲区长度最多为 $O(n)$;没有保存整棵回溯树。

关键点总结

  • 回溯只需在前两个切点处分支,第三项起由前两项之和唯一确定。
  • 不能依赖固定宽度整数;字符串加法同时解决 Java 和 Go 的大数溢出问题。
  • 验证成功的条件是整个字符串被恰好消费完,不是只匹配出一个和。
  • 前导零规则是“长度大于 1 时首位不能为 0”,所以 "000" 合法。
  • 剩余长度不足下一项的最小可能长度时可以安全剪枝。

易错点总结

  • 使用 Long.parseLongstrconv.ParseInt,遇到超长数字会溢出。
  • 完全禁止数字 0 会错判 "000";允许 "01" 又会引入非法切分。
  • 只验证第三项就返回,会把后续不满足关系的字符串误判为合法。
  • 第二个切点取到字符串末尾,会在没有第三个数时错误成功。
  • 字符串加法遗漏最终进位或忘记反转,会在 "99" + "1" 这类用例中出错。
  • Go 中截取前缀前未检查剩余长度,会触发切片越界。

相似题目

题目 难度 考察点
842. 将数组拆分成斐波那契序列 中等 同构问题并要求还原具体方案
93. 复原 IP 地址 中等 定段数切分与合法性剪枝
131. 分割回文串 中等 回溯枚举切点并逐段校验
140. 单词拆分 II 困难 记忆化搜索枚举全部拆分
472. 连接词 困难 字典树配合拆分可行性判定
241. 为运算表达式设计优先级 中等 分治按运算符切分并合并结果