重新审视问题
这是 Ruby 中的一个简单解决方案,它检查区间 [m,n] 中的每个整数,确定其数字在标准以 10 为底的位置系统中的字符串,并对出现的 0 位进行计数:
def brute_force(m, n)
if m > n
return 0
end
z = 0
m.upto(n) do |k|
z += k.to_s.count('0')
end
z
end
如果你在交互式 Ruby shell 中运行它,你会得到
irb> brute_force(1,100)
=> 11
这很好。但是使用问题示例中的区间界限
m = 1234567890
n = 2345678901
您会意识到这需要相当长的时间。在我的机器上它确实需要超过几秒钟,到目前为止我不得不取消它。
因此,真正的问题不仅是要提出正确的零计数,而且要比上述蛮力解决方案更快地做到这一点。
复杂性:运行时间
蛮力解法需要执行n-m+1次搜索以10为底的数字k,其长度为floor(log_10(k))+1,所以不会使用超过
O(n (log(n)+1))
字符串数字访问。慢示例的 n 大约为 n = 10^9。
降低复杂性
荣一鸣的回答是第一次尝试降低问题的复杂性。
如果计算区间[m,n]的零个数的函数是F(m,n),那么它有性质
F(m,n) = F(1,n) - F(1,m-1)
因此只要寻找一个最有可能具有该属性的更简单的函数 G 就足够了
G(n) = F(1,n)。
分而治之
为函数 G 提出一个封闭的公式并不容易。例如。
区间 [1,1000] 包含 192 个零,但区间 [1001,2000] 包含 300 个零,因为像第一个区间中的 k = 99 这样的情况将对应于第二个区间中的 k = 1099,这会产生另一个零位数。 k=7 将显示为 1007,产生另外两个零。
人们可以尝试用更简单的问题实例的解决方案来表达某些问题实例的解决方案。这种策略在计算机科学中被称为分而治之。如果在某种复杂程度可以解决问题实例,并且如果可以从更简单问题的解决方案中推断出更复杂问题的解决方案,那么它就可以工作。这自然会导致递归公式。
例如我们可以为 G 的受限版本制定解决方案,该版本仅适用于某些论点。我们称它为 g,它被定义为 9、99、999 等,并且对于这些参数将等于 G。
可以使用这个递归函数来计算:
# zeros for 1..n, where n = (10^k)-1: 0, 9, 99, 999, ..
def g(n)
if n <= 9
return 0
end
n2 = (n - 9) / 10
return 10 * g(n2) + n2
end
请注意,此函数比蛮力方法快得多:要计算区间 [1, 10^9-1] 中的零,与问题中的 m 相当,它只需要 9 次调用,其复杂度是
O(log(n))
再次注意,这个 g 不是针对任意 n 定义的,仅针对 n = (10^k)-1。
g的推导
首先要找到函数 h(n) 的递归定义,
如果十进制表示具有前导零,则将 1 到 n = (10^k) - 1 的数字中的零计数。
示例:h(999) 计算数字表示的零位:
- 001..009
- 010..099
- 100..999
结果将是 h(999) = 297。
使用 k = floor(log10(n+1)), k2 = k - 1, n2 = (10^k2) - 1 = (n-9)/10 函数 h 是
h(n) = 9 [k2 + h(n2)] + h(n2) + n2 = 9 k2 + 10 h(n2) + n2
初始条件 h(0) = 0。它允许将 g 表示为
g(n) = 9 [k2 + h(n2)] + g(n2)
初始条件 g(0) = 0。
从这两个定义中,我们也可以定义 h 和 g 之间的差异 d,同样是一个递归函数:
d(n) = h(n) - g(n) = h(n2) - g(n2) + n2 = d(n2) + n2
初始条件 d(0) = 0。尝试一些示例会导致几何级数,例如d(9999) = d(999) + 999 = d(99) + 99 + 999 = d(9) + 9 + 99 + 999 = 0 + 9 + 99 + 999 = (10^0)-1 + (10 ^1)-1 + (10^2)-1 + (10^3)-1 = (10^4 - 1)/(10-1) - 4。这给出了封闭形式
d(n) = n/9 - k
这允许我们仅用 g 来表达 g:
g(n) = 9 [k2 + h(n2)] + g(n2) = 9 [k2 + g(n2) + d(n2)] + g(n2) = 9 k2 + 9 d(n2 ) + 10 g(n2) = 9 k2 + n2 - 9 k2 + 10 g(n2) = 10 g(n2) + n2
G的推导
使用上述定义并将表示的 k 位命名为 q_k, q_k2, .., q2, q1 我们首先将 h 扩展为 H:
H(q_k q_k2..q_1) = q_k [k2 + h(n2)] + r (k2-kr) + H(q_kr..q_1) + n2
对于 q_1
注意附加定义 r = q_kr..q_1。要了解为什么需要它,请查看示例 H(901),其中对 H 的下一级调用是 H(1),这意味着数字字符串长度从 k=3 缩小到 kr=1,需要额外的填充r (k2-kr) 零位。
使用它,我们也可以将 g 扩展到 G:
G(q_k q_k2..q_1) = (q_k-1) [k2 + h(n2)] + k2 + r (k2-kr) + H(q_kr..q_1) + g(n2)
对于 q_1
注意:很可能可以像上面 g 的情况一样简化上述表达式。例如。试图仅用 G 来表达 G 而不是使用 h 和 H。我将来可能会这样做。以上已经足够实现快速归零计算了。
测试结果
recursive(1234567890, 2345678901) =
987654304
expected:
987654304
success
详情请参阅source 和log。
更新:我根据该竞赛中更详细的问题描述更改了源和日志(允许 0 作为输入,处理无效输入,第二个更大的示例)。