这是一个非常有趣的问题。与其跳入最终的整体算法,我想我应该从一个不太合理的算法开始,然后对其进行一系列修改,最终得到最终的 O(nk) 时间算法。
这种方法结合了多种不同的技术。主要技术是计算数字上的滚动余数的想法。例如,假设我们要查找字符串中所有为 k 倍数的 前缀。我们可以通过列出所有前缀并检查每个前缀是否是 k 的倍数来做到这一点,但这至少需要 Θ(n2) 时间,因为有 Θ(n2 ) 不同的前缀。然而,我们可以通过更聪明一点在时间 Θ(n) 中做到这一点。假设我们知道我们已经读取了字符串的前 h 个字符,并且我们知道以这种方式形成的数字的其余部分。我们也可以用它来说明字符串的前 h+1 个字符的其余部分,因为通过附加该数字,我们获取现有数字,将其乘以 10,然后添加下一个数字。这意味着如果我们有 r 的余数,那么我们的新余数是 (10r + d) mod k,其中 d 是我们发现的数字。
这里有一个快速的伪代码来计算字符串中 k 的倍数的前缀数量。它在时间 Θ(n) 中运行:
remainder = 0
numMultiples = 0
for i = 1 to n: // n is the length of the string
remainder = (10 * remainder + str[i]) % k
if remainder == 0
numMultiples++
return numMultiples
我们将使用这种初始方法作为整个算法的构建块。
所以现在我们有一个算法可以找到我们的字符串的 前缀 的数量是 k 的倍数。我们如何将其转换为一种算法,该算法可以找到 k 的倍数的 子字符串 的数量?让我们从一种不太奏效的方法开始。如果我们计算原始字符串中所有 k 的倍数的前缀,然后删除字符串的第一个字符并计算剩余部分的前缀,然后删除第二个字符并计算剩余部分的前缀,等等?这最终会找到每个子字符串,因为原始字符串的每个子字符串都是该字符串某些后缀的前缀。
这是一些粗略的伪代码:
numMultiples = 0
for i = 1 to n:
remainder = 0
for j = i to n:
remainder = (10 * remainder + str[j]) % k
if remainder == 0
numMultiples++
return numMultiples
例如,在字符串14917 上运行此方法以查找7 的倍数,将会找到这些字符串:
- 字符串
14917:查找14、1491、14917
- 字符串
4917:查找49,
- 字符串
917:查找91、917
- 字符串
17:什么也没找到
- 字符串
7:查找7
这种方法的好消息是它会找到所有有效的子字符串。坏消息是它的运行时间为 Θ(n2)。
但是让我们看看我们在这个例子中看到的字符串。例如,查看通过搜索整个字符串的前缀找到的子字符串。我们找到了其中三个:14、1491 和 14917。现在,看看这些字符串之间的“差异”:
-
14 和14917 的区别是917。
-
14 和1491 的区别是91
-
1491 和14917 的区别是7。
请注意,这些字符串中的每一个的差异本身就是 14917 的子字符串,它是 7 的倍数,事实上,如果您查看我们稍后在算法运行中匹配的其他字符串,我们会'也会找到这些其他字符串。
这不是巧合。如果您有两个具有相同前缀的数字是相同数字 k 的倍数,那么它们之间的“差异”也将是 k 的倍数。 (检查这方面的数学是一个很好的练习。)
所以这表明我们可以采取另一条路线。假设我们找到原始字符串的所有前缀都是 k 的倍数。如果我们能找到所有这些,我们就可以计算出这些前缀之间有多少成对的差异,并可能避免多次重新扫描。这不一定会找到所有内容,但它会找到可以通过计算两个前缀的差异形成的所有子字符串。对所有后缀重复此操作 - 并注意不要重复计算 - 确实可以加快速度。
首先,假设我们找到了 r 个不同的字符串前缀,它们是 k 的倍数。如果我们包含差异,我们只找到了多少个子字符串?好吧,我们找到了 k 个字符串,每对(无序)元素加上一个额外的字符串,计算结果为 k + k(k-1)/2 = k(k+1)/2 个子字符串。不过,我们仍然需要确保不会重复计算。
要查看我们是否重复计算某些内容,我们可以使用以下技术。当我们计算沿字符串的滚动余数时,我们将存储在每个条目之后找到的余数。如果在计算滚动余数的过程中,我们在某个时刻重新发现了已经计算过的余数,我们就知道我们正在做的工作是多余的;之前对字符串的一些扫描已经计算了这个余数,并且我们从现在开始发现的任何东西都已经找到了。
把这些想法放在一起就得到了这个伪代码:
numMultiples = 0
seenRemainders = array of n sets, all initially empty
for i = 1 to n:
remainder = 0
prefixesFound = 0
for j = i to n:
remainder = (10 * remainder + str[j]) % k
if seenRemainders[j] contains remainder:
break
add remainder to seenRemainders[j]
if remainder == 0
prefixesFound++
numMultiples += prefixesFound * (prefixesFound + 1) / 2
return numMultiples
那么效率如何?乍一看,由于外部循环,它看起来像在 O(n2) 时间内运行,但这不是一个紧密的界限。请注意,每个元素最多只能在内循环中传递 k 次,因为在那之后没有任何剩余部分仍然是空闲的。因此,由于每个元素最多被访问 O(k) 次,总共有 n 个元素,因此运行时间为 O(nk),符合您的运行时间要求。