1. 题目背景与核心问题解析
这道LeetCode困难题编号1416,标题为"Restore The Array"(恢复数组)。题目描述了一个看似简单但实际颇具挑战性的字符串处理问题:给定一个由数字组成的字符串s和一个整数k,我们需要计算出所有可能的方案数,将这个字符串分割成若干个子串,使得每个子串表示的数字都不大于k,并且不包含前导零(除了数字0本身)。
我第一次看到这个题目时,被它的简洁描述所迷惑,以为只是一个简单的字符串分割问题。但深入分析后才发现,这实际上是一个典型的动态规划应用场景,涉及到字符串处理、数值边界判断和模运算等多个计算机科学核心概念。
2. 问题分析与解法思路
2.1 题目具体要求拆解
题目给出的具体条件是:
- 输入字符串s只包含数字('0'-'9')
- 整数k的范围是1到10^9
- 需要返回所有有效分割方案的数量,结果对10^9+7取模
- 有效分割必须满足:
- 每个子串表示的数字≤k
- 子串不能有前导零(即长度大于1的子串不能以'0'开头)
2.2 暴力解法与复杂度分析
最直观的解法是尝试所有可能的分割方式,然后统计符合条件的方案数。这种方法的时间复杂度是O(2^n),其中n是字符串长度。对于n=100的情况(题目给出的上限),这样的复杂度显然无法接受。
注意:在实际编码面试中,即使你能想到暴力解法,也应该立即意识到这不是最优解,并主动向面试官说明这一点。
2.3 动态规划思路建立
通过分析,我们可以发现这个问题具有最优子结构性质:
- 整个字符串的有效分割数,可以分解为考虑不同长度的前缀的有效分割数
- 子问题的解可以被重复利用
这提示我们可以使用动态规划来解决。定义dp[i]表示前i个字符(s[0..i-1])的有效分割方案数。我们的目标是求dp[n],其中n是字符串长度。
3. 动态规划实现详解
3.1 状态转移方程推导
对于每个位置i,我们需要考虑所有可能的j(j < i),使得子串s[j..i-1]满足:
- 没有前导零(除非子串就是"0")
- 数值不超过k
如果满足条件,那么dp[i] += dp[j]
初始条件是dp[0] = 1(空字符串有1种分割方式)
3.2 边界条件处理
实现时需要特别注意几个边界条件:
- 子串长度不能超过k的位数(因为k最多是10^9,即最多10位)
- 处理大数比较(当子串长度等于k的位数时)
- 前导零的特殊处理
3.3 代码实现示例
MOD = 10**9 + 7 def numberOfArrays(s: str, k: int) -> int: n = len(s) dp = [0] * (n + 1) dp[0] = 1 max_len = len(str(k)) for i in range(1, n + 1): for j in range(i - 1, max(-1, i - max_len - 1), -1): if s[j] == '0': continue num = int(s[j:i]) if num <= k: dp[i] = (dp[i] + dp[j]) % MOD else: break return dp[n]4. 算法优化与复杂度分析
4.1 时间复杂度优化
原始实现中,内层循环最多执行min(i, max_len+1)次,其中max_len是k的位数(最多10)。因此总时间复杂度是O(n * 10) = O(n),对于n=1e5也完全可以接受。
4.2 空间复杂度优化
dp数组只需要O(n)空间,没有使用额外数据结构,空间复杂度已经是理论下限。
4.3 大数处理技巧
当k接近10^9时,直接使用int转换可能会在某些语言中导致溢出。可以提前比较字符串长度和k的位数:
- 如果子串长度 > k的位数,直接跳过
- 如果长度相等,可以逐位比较字符串和k的字符串表示
5. 常见错误与调试技巧
5.1 前导零处理不当
常见错误是忘记处理前导零的情况。例如:
- 输入"001", k=1
- 错误解法可能会认为"0|0|1"是有效分割
- 正确解法应该识别到"00"开头的子串无效
5.2 数值越界问题
当k=1e9时,某些语言中int可能无法处理更大的数。解决方案:
- 提前比较字符串长度
- 使用更高精度的整数类型
5.3 模运算遗漏
必须在每次加法后立即取模,否则中间结果可能溢出。例如:
# 错误写法 dp[i] += dp[j] dp[i] %= MOD # 正确写法 dp[i] = (dp[i] + dp[j]) % MOD6. 测试用例设计建议
6.1 常规测试用例
- "1317", k=2000 → 8
- "2020", k=30 → 1
- "1234", k=50 → 5
6.2 边界测试用例
- "0", k=0 → 1
- "1000000000", k=1000000000 → 1
- "1000", k=10 → 0
6.3 性能测试用例
- 长字符串(1e5个'1'),k=1e9 → 应快速返回结果
7. 实际应用场景延伸
虽然这是一个算法题,但类似的字符串分割问题在实际开发中很常见,例如:
- IP地址恢复(LeetCode 93)
- 文本分词处理
- 序列化数据解析
掌握这类动态规划解法,可以帮助我们更好地处理各种字符串解析问题。我在实际工作中就曾遇到过类似的日志解析需求,需要将连续的日志标识符分割成有效的消息块,当时的解决方案就借鉴了这道题的思路。