我相信您可以通过使用后缀树在线性时间内做到这一点。这当然不是一个轻量级的解决方案——祝你好运编写一个线性时间算法来构建一个可变大小字母表的后缀树! - 但它表明这是可能的。
这就是想法。首先为数组构建一个后缀树,不将其视为数字列表,而是将其视为字符串,其中每个字符都是一个数字。由于您知道所有数字最多为 1,000,因此不同字符的数量是一个常数,因此使用快速后缀树构建算法(例如 SA-IS),您可以在 O(n) 时间内构建后缀树。
后缀树在这里是一个很好的结构,因为它们将相同子字符串的重复副本折叠在一起,形成重叠的组,这使得删除重复数据变得更容易。例如,如果模式 [1, 3, 7] 在数组中出现多次,则根将恰好包含一个以 [1, 3, 7] 开头的路径。
现在的问题是如何从后缀树到不同子数组的数量。现在,让我们解决一个更简单的问题 - 你如何计算不同子数组的数量,周期,完全忽略对奇数和偶数的限制?幸运的是,这是一个经过充分研究的问题,可以在线性时间内解决。本质上,后缀树中编码的每个前缀都对应于原始数组的一个不同的子数组,因此您只需计算有多少个前缀。这可以通过递归地遍历树来完成,对于树中的每条边,沿该边有多少字符。这可以在 O(n) 时间内完成,因为长度为 n 的数组/字符串的后缀树有 O(n) 个节点,并且我们花费恒定的时间处理每个节点(只需查看其上方的边。 )
所以现在我们只需要加入对允许使用的偶数个数的限制。这使事情有点复杂,但原因很微妙。直觉上,这似乎不应该是一个问题。毕竟,我们可以对后缀树进行 DFS,然后在我们遍历的路径上计算偶数的数量,一旦超过 m,就停止。
这种方法的问题在于,即使后缀树中有 O(n) 个节点,边缘也会隐式编码长度可以与 n 本身一样高的范围。因此,扫描边缘的行为本身可能会将运行时间增加到 Ω(n2):访问 Θ(n) 边缘并为每个边缘做 Ω(n) 工作。
不过,我们可以稍微加快速度。后缀树中的每条边通常表示为原始数组中的一对索引 [start, stop]。所以让我们想象一下,作为一个额外的预处理步骤,我们构建了一个表 Evens,使得 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),因为字符串中最多有 1000 个不同的数字,而 1000 是一个常数。
在 O(n) 时间内计算表 Even[n]。
在 O(n) 时间内计算表 KthEven[n]。
对树执行 DFS,跟踪目前遇到的偶数个数。当遇到边 [start, stop] 时,使用 Even in time O(1) 计算该范围内有多少偶数。如果低于限制,请继续递归。如果没有,请使用 KthEven 表计算出在 O(1) 时间内有多少边缘可用。无论哪种方式,将不同子数组数量的全局计数增加当前边的可用长度。这对后缀树中的每个 O(n) 边执行 O(1) 工作,总共 O(n) 工作。
呸!这不是一个容易的问题。我想有一些方法可以简化这种结构,我欢迎 cmets 和关于如何做到这一点的建议。但这表明确实有可能在O(n)时间内解决这个问题,这不是立即显而易见的!