TL;DR:如果你做对了,你可以将计数快一千倍n接近 10**5,并且由于更好的算法使用的时间与数字的位数成正比n,它可以很容易地处理偶数n对于 64 位整数来说太大了。
像这样的谜题(“在从 x 到 y 的数字中,有多少......?”)经常出现这种情况,关键是找到一种方法来计算聚合计数,理想情况下在 O(1) 中,对于范围大。对于数字字符串表示的组合,一个方便的范围通常类似于所有数字的集合,其字符串表示为给定大小,可能带有特定前缀。换句话说,[prefix*10⁴, prefix*10⁴+9999] 形式的范围,其中下限中的 0 与上限中的 9 的数量以及乘数中 10 的指数相同。 (实际上通常使用半开范围更方便,其中下限包括在内,上限是排他性,所以上面的例子是[prefix*10⁴, (prefix+1)*10⁴)。)
另请注意,如果问题是计算 [x, y) 的计数,而您只知道如何计算 [0, y),那么您只需进行两次计算,因为
count [x, y) == count [0, y) - count [0, x)
该恒等式是半开区间允许的简化之一。
这可以很好地解决这个问题,因为很清楚一个数字有多少次d出现在给定前缀的所有 k 位后缀的集合中。 (在 10ķ后缀,每个数字与其他数字具有相同的频率;共有ķ×10ķ这 10 位中的数字ķ,并且由于所有数字都具有相同的计数,因此该计数必须是ķ×10ķ-1.) 然后你只需要添加前缀的位数,但前缀正好出现 10ķ次,并且每个人贡献相同的计数。
所以你可以取一个像 72483 这样的数字,并将其分解为以下范围,这些范围大致对应于 72483 中的数字之和,加上一些包含较少数字的范围。
- [0, 9]
- [10, 99]
- [100, 999]
- [1000, 9999]
- [10000, 19999]
- [20000, 29999]
- [30000, 39999]
- [40000, 49999]
- [50000, 59999]
- [60000, 69999]
- [70000, 70999]
- [71000, 71999]
- [72000, 72099]
- [72100, 72199]
- [72200, 72299]
- [72300, 72399]
- [72400, 72409]
- [72410, 72419]
- [72420, 72429]
- [72430, 72439]
- [72440, 72449]
- [72450, 72459]
- [72460, 72469]
- [72470, 72479]
- [72480, 72480]
- [72481, 72481]
- [72482, 72482]
- [72483, 72483]
然而,在下面的代码中,我使用了一个稍微不同的算法,结果证明它有点短。它考虑写出从 0 到 n 的所有数字(包括前导零)的矩形,然后计算每列的计数。连续整数矩形中的一列数字遵循简单的循环模式;通过从列的完全重复部分开始,可以轻松计算频率。完成重复后,其余数字按顺序排列,除最后一位外,每一位出现相同次数。通过在纸上画一个小例子可能最容易理解,但下面的代码也应该相当清楚(我希望)。
这样做的一个问题是它计算实际不存在的前导零,因此需要通过减去前导零计数来纠正它。幸运的是,这个计数非常容易计算。如果您考虑一个以五位数字结尾的范围(它本身不能以零开头,因为如果它以零开头,它就不是真正的五位数字),那么您可以看到该范围包括:
- 10000 个数字以零开头
- 还有 1000 个数字,其中有第二个前导零
- 还有 100 个数字,其中有第三个前导零
- 还有 10 个数字,其中有第四个前导零
没有数字有五个前导零,因为我们这样写 0,而不是空字符串。
加起来是 11110,很容易看出它是如何概括的。该值可以在没有循环的情况下计算,如 (10⁵ − 1) / 9 − 1。该校正在以下函数的末尾完成:
def countd(m, s=(0,2,4)):
if m < 0: return 0
m += 1
rv = 0
rest = 0
pos = 1
while True:
digit = m % 10
m //= 10
rv += m * pos * len(s)
for d in s:
if digit > d:
rv += pos
elif digit == d:
rv += rest
if m == 0:
break
rest += digit * pos
pos *= 10
if 0 in s:
rv -= (10 * pos - 1) // 9 - 1
return rv
几乎可以肯定该代码可以收紧。我只是想把算法弄下来。但是,实际上,它的执行时间以微秒而不是毫秒为单位,即使对于更大的值n.
这是凯利基准的更新;我删除了其他解决方案,因为它们的最后一个值花费了太长时间n:
Try it online!