为您提供了一个整数数组A,每个整数都在[0,1000]范围内,同时还有一些数字m。例如,您可能会得到以下输入:
A=[5,6,7,8] m=1
问题是要尽可能高效地确定数组A中有多少个不同的非空子数组,最多包含m个偶数。例如,对于上面的数组,有八个不同的子数组,最多具有一个偶数,如下所示:
[(5, 6, 7), (6, 7), (5, 6), (8), (5), (6), (7), (7, 8)]
这是我到目前为止的解决方案,它的运行时间为O(n 3 ):
def beautiful(A, m):
subs = [tuple(A[i:j]) for i in range(0, len(A)) for j in range(i + 1, len(A) + 1)]
uniqSubs = set(subs)
return len([n for n in uniqSubs if sum(int(i) % 2 == 0 for i in n)<=m ])
是否有更好的解决方案-理想情况下,该算法以线性时间运行或至少达到O(n ^ 2)?
答案 0 :(得分:2)
我相信您可以通过使用后缀树在线性时间内完成此操作。这当然不是一个轻量级的解决方案-祝您好运,编写线性时间算法以构建带有可变大小字母的后缀树! -但它表明有可能。
这是主意。首先为数组构建后缀树,将其不视为数字列表,而是视为字符串,其中每个字符都是数字。因为您知道所有数字最多为1,000,所以不同字符的数量是一个常数,因此使用快速后缀树构建算法(例如SA-IS),可以在时间O(n)内构建后缀树。
后缀树在这里是一个很好的结构,因为它们将相同子字符串的重复副本折叠在一起成为重叠的组,这使重复数据删除变得更加容易。例如,如果模式[1、3、7]在数组中出现多次,则根目录将只包含一个以[1、3、7]开头的路径。
现在的问题是如何从后缀树转到数量不同的子数组。现在,让我们解决一个更简单的问题-如何计算独立的子数组,周期的数量,而完全忽略对奇数和偶数的限制?幸运的是,这是一个经过充分研究的问题,可以在线性时间内解决。本质上,后缀树中编码的每个前缀都对应于原始数组的一个不同子数组,因此您只需要计算有多少个前缀。这可以通过递归地遍历树并为树中的每个边累加沿该边有多少个字符来完成。之所以可以在时间O(n)中完成,是因为长度为n的数组/字符串的后缀树具有O(n)个节点,并且我们花费固定的时间来处理每个节点(仅通过查看其上方的边缘即可)。 )
因此,现在我们只需要合并对允许使用的偶数数量的限制。这使事情有些复杂,但是原因很微妙。凭直觉,这似乎不应该是问题。毕竟,我们可以对后缀树做一个DFS,然后,对经过的路径上的偶数进行计数,一旦超过m,就停止。
此方法的问题在于,即使后缀树中有O(n)个节点,边缘也隐式地对长度可能高达n的范围进行编码。结果,扫描边缘的行为本身可能会使运行时间增加到Ω(n 2 ):访问Θ(n)边缘并为每个边缘进行Ω(n)工作。
但是,我们可以加快速度。后缀树中的每个边通常表示为原始数组中的一对索引[start,stop]。因此,让我们想象一下,作为一个附加的预处理步骤,我们构建了一个偶数表,使Evens [n]返回数组中偶数个数的位数,直到并包括位置n。然后,我们可以通过计算Evens [start]-Evens [stop]来计算任意范围[start,stop]中的偶数个数。这需要花费时间O(1),这意味着我们可以将沿路径遇到的偶数的数量与所跟随的边数成正比,而不是所遇到的字符数成正比。
...除了有一个并发症。如果我们有一个很长的边沿,在读取该边沿之前,我们知道自己低于偶数限制,而在读取该边沿之后,我们知道自己已经超过了限制,会发生什么?这意味着我们需要中途停止,但是我们不确定确切的位置。这可能需要我们在边缘上进行线性搜索以找到交叉点,然后运行时就结束了。
但是幸运的是,有一个摆脱这种小困境的方法。 (下一部分包含@Matt Timmermans的改进)。作为预处理的一部分,除了Evens数组之外,还要构建第二个表KthEven,其中KthEven [i]返回数组中第k个偶数的位置。这可以使用Evens数组在时间O(n)中建立。一旦有了这个,让我们想象一下您的劣势,那将使您超出极限。如果知道到目前为止已经遇到了多少个偶数,则可以确定将使您超出限制的偶数索引。然后,您可以通过在时间O(1)索引到KthEven表中来查找该偶数在哪里。这意味着我们只需要在后缀树的每个边缘上花费O(1)个工作,就可以将运行时间降低到O(n)!
因此,回顾一下,这是解决此问题的线性时间解决方案:
使用快速后缀树构建算法(例如SA-IS或Ukkonen的算法)为阵列构建后缀树。这需要时间O(n),因为字符串中最多有1,000个不同的数字,而1,000是一个常数。
在时间O(n)中计算表Even [n]。
在时间O(n)中计算表KthEven [n]。
在树上进行DFS,跟踪到目前为止遇到的偶数个数。遇到边[开始,停止]时,请使用时间O(1)中的偶数来计算该范围内的偶数。如果低于限制,请继续递归。如果不是,请使用KthEven表找出在时间O(1)中可以使用多少边。无论哪种方式,都可以通过当前边的可用长度来增加不同子数组数目的全局计数。对于后缀树中的每个O(n)边,O(1)确实起作用,总共进行了O(n)个工作。
Ph!这不是一个容易的问题。我想有某种方法可以简化此构造,并且欢迎提出意见和建议。但这表明确实有可能在O(n)时间内解决此问题,这并不是立即显而易见的!