题目描述(中等难度)

306、Additive Number

Additive number is a string whose digits can form additive sequence.

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.

Given a string containing only digits '0'-'9', write a function to determine if it's an additive number.

Note: Numbers in the additive sequence cannot have leading zeros, so sequence 1, 2, 03 or 1, 02, 3 is invalid.

Example 1:

Input: "112358"
Output: true
Explanation: The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. 
             1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8

Example 2:

Input: "199100199"
Output: true
Explanation: The additive sequence is: 1, 99, 100, 199. 
             1 + 99 = 100, 99 + 100 = 199

Constraints:

  • num consists only of digits '0'-'9'.
  • 1 <= num.length <= 35

Follow up: How would you handle overflow for very large input integers?

给一个字符串,判断字符串符不符合某种形式。就是从开头选两个数,它俩的和刚好是下一个数,然后第 2 个数和第 3 数字的和又是下一个数字,以此类推。

解法一

理解了题意的话很简单,可以勉强的归到回溯法的问题中,我们从暴力求解的角度考虑。

首先需要两个数,我们可以用两层 for 循环依次列举。

//i 表示第一个数字的结尾(不包括 i)
for (let i = 1; i < num.length; i++) {
    //j 表示从 i 开始第二个数字的结尾(不包括 j)
    for (let j = i + 1; j < num.length; j++) {
        let num1 = Number(num.substring(0, i));
        let num2 = Number(num.substring(i, j));
    }
}

然后需要处理下特殊情况,将以 0 开头但不是0的数字去掉,比如 023 这种是不合法的,

//i 表示第一个数字的结尾(不包括 i) 
for (let i = 1; i < num.length; i++) {
    // 0 开头, 并且当前数字不是 0
    if (num[0] === '0' && i > 1) {
        return false;
    }
    //j 表示从 i 开始第二个数字的结尾(不包括 j)
    for (let j = i + 1; j < num.length; j++) {
        // 0 开头, 并且当前数字不是 0  
        if (num[i] === '0' && j - i > 1) {
            break;
        }
        let num1 = Number(num.substring(0, i));
        let num2 = Number(num.substring(i, j));
    }
}

有了这两个数字,下边的就好说了。

只需要计算 sum = num1 + num2 ,然后看 sum 在不在接下来的字符串中。

如果不在,那么就考虑下一个 num1num2

如果在的话,num1 就更新为 num2num2 更新为 sum ,有了新的 num1num2 ,然后继续按照上边的步骤考虑。

举个例子,

1 2 2 1 4 16
    ^ ^
    i j

num1 = 12
num2 = 2
sum = num1 + num2 = 14

接下来的数字刚好是 14, 那么就更新 num1 和 num2

num1 = num2 = 2
num2 = sum = 14

然后继续判断即可。

代码的话,我们可以写成递归的形式。

/**
 * @param {string} num
 * @return {boolean}
 */
var isAdditiveNumber = function (num) {
    if (num.length === 0) {
        return true;
    }
    for (let i = 1; i < num.length; i++) {
        if (num[0] === '0' && i > 1) {
            return false;
        }
        for (let j = i + 1; j < num.length; j++) {
            if (num[i] === '0' && j - i > 1) {
                break;
            }
            let num1 = Number(num.substring(0, i));
            let num2 = Number(num.substring(i, j));
            if (isAdditiveNumberHelper(num.substring(j), num1, num2)) {
                return true;
            }
        }
    }
    return false;
};

function isAdditiveNumberHelper(num, num1, num2) {
    if (num.length === 0) {
        return true;
    }
    //依次列举数字,判断是否等于 num1 + num2
    for (let i = 1; i <= num.length; i++) {
        //不考虑以 0 开头的数字
        if (num[0] === '0' && i > 1) {
            return false;
        }
        let sum = Number(num.substring(0, i));
        if (num1 + num2 === sum) {
            //传递剩下的字符串以及新的 num1 和 num2
            return isAdditiveNumberHelper(num.substring(i), num2, sum);
            //此时大于了 num1 + num2, 再往后遍历只会更大, 所以直接结束
        } else if (num1 + num2 < sum) {
            break;
        }
    }
    return false;
}

主要思想就是上边的了,看了 这里 的分析,代码的话,可以简单的优化下。

第一点,如果两个数的位数分别是 a 位和 b 位,a >= b,那么它俩相加得到的和至少是 a 位。比如 100 + 99 得到 199,依旧是三位数。

所以我们的 num1num2 其中的一个数的位数一定不能大于等于字符串总长度的一半,不然的话剩下的字符串一定不等于 num1num2 的和,因为剩下的长度不够了。

第二点,上边的代码中 isAdditiveNumberHelper 函数,略微写的复杂了些,我们可以直接利用 StringstartsWith 函数,判断 num1 + num2 是不是字符串的开头即可。

基于上边两点,优化后的代码如下。

/**
 * @param {string} num
 * @return {boolean}
 */
var isAdditiveNumber = function (num) {
    if (num.length === 0) {
        return true;
    }
    //这里取了等号,是因为长度是奇数的时候,除以二是向下取整
    for (let i = 1; i <= num.length / 2; i++) {
        if (num[0] === '0' && i > 1) {
            return false;
        }
        for (let j = i + 1; j < num.length; j++) {
            if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) {
                break;
            }
            let num1 = Number(num.substring(0, i));
            let num2 = Number(num.substring(i, j));
            if (isAdditiveNumberHelper(num.substring(j), num1, num2)) {
                return true;
            }
        }
    }
    return false;
};

function isAdditiveNumberHelper(num, num1, num2) {
    if (num.length === 0) {
        return true;
    }
    return num.startsWith(num1 + num2) && isAdditiveNumberHelper(num.substring((num1+num2+'').length), num2, num1 + num2);
}

当然,我们最初的分析很明显是迭代的形式,我们内层也可以直接写成循环,不需要递归函数。

/**
 * @param {string} num
 * @return {boolean}
 */
var isAdditiveNumber = function (num) {
    if (num.length === 0) {
        return true;
    }
    for (let i = 1; i <= num.length / 2; i++) {
        if (num[0] === '0' && i > 1) {
            return false;
        }
        for (let j = i + 1; j < num.length; j++) {
            if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) {
                break;
            }
            let num1 = Number(num.substring(0, i));
            let num2 = Number(num.substring(i, j));
            let temp = num.substring(j);
            while(temp.startsWith(num1 + num2)) {
                let n = num1;
                num1 = num2;
                num2 = num1 + n;
                temp = temp.substring((num2 + '').length);
                if (temp.length === 0) {
                    return true;
                }
            } 
        }
    }
    return false;
};

还有一个扩展,How would you handle overflow for very large input integers?

让我们考虑数字太大了怎么办,此时解决大数相加的问题即可,所有的操作在字符串层面上进行。参考 第 2 题

这道题主要的想法就是列举所有情况,但总觉得先单独列举两个数字不够优雅,思考了下怎么把它合并到递归中,无果,悲伤。

上边其实是一种思路,只是在写法上可能有所不同,唯一的优化就是列举数字的时候考虑一半即可,但时间复杂度不会变化。

之所以说勉强归到回溯法,是因为遍历路径的感觉是,先选两个数,然后一路到底,失败的话不是回到上一层,而是直接回到开头,然后重新选取两个数,继续一路到底,不是典型的回溯。

results matching ""

    No results matching ""