对于给定的实数数组,Kadane的动态编程算法可以在线性时间内找到数组中的最大和子区间。但是,假设我们已经进行了一些预处理以获得最佳解决方案以及任何所需的辅助信息,然后我们给出了一个转换来交换数组中的两个元素。是否有一种方案可以在子线性时间内更新最佳解决方案子区间,并允许后续转换的未来更新?我正在为大小为o(N^2)
的数组寻找预处理时间和额外内存为N
。
答案 0 :(得分:0)
这是一种预处理列表的方法,在一般情况下,大大缩小列表的长度。考虑到原始列表中某个条目的更改,更新预处理信息也不难。
预处理过程的想法是某些元素集合可以组合并在逻辑上作为单个元素处理。例如,如果列表中有两个正数,则在
中... -1 3 4 -2 ...
那么你永远不会想要在你的最大子集数中加入3,除非你还包括4.所以,你可以在逻辑上将列表的这部分视为
... -1 7 -2 ...
相邻的负数也是如此。因此,从现在开始,我们可以假设列表条目的符号交替出现。
我们还需要另外两种简化:
如果它们之间有两个带正数的正数,并且该负数的数量小于两个正数中的任何一个,那么您绝不希望在最大子集和中包含正数而不包括其他。例如:
... 8 -2 4 ...
可以视为
... 10 ...
这是因为如果我们包含4
,那么我们也可以包含-2 + 8 = 6
,同样来自另一方。
另一种简化是负数的模拟。如果有两个负数,它们之间有正数,并且正数的数量小于负数,那么我们可以将三元组视为单个数字。例如:
... -10 4 -12 ...
可以视为
... -18 ...
很难理解为什么这是有效的。假设我们的最大子集和包括这三个数中的一个,比如-12
。当然它包括4
,因为这会使子集总和更大。但它当然也必须包括-10
,因为如果没有,那么我们可以从区间的末尾切掉4 - 12 = -8
并使我们的子集和更大。直觉上,如果这个三元组存在于最大子集和区间内,则它位于区间中间的某个位置。
您需要递归地应用这三个简化,直到没有在列表中产生更改。这可能需要很长时间,但是可以通过跟踪自上次迭代以来列表的哪些区域发生了变化并且仅使用重复的应用程序命中这些区域来缩短这一点。
优点是,在应用这些修改后,最大子集总和恰好是(简化)列表中剩余的最大正条目!这是一个证据:
假设我们的列表按照上述简化进行了简化(意味着应用三者中的任何一个都不做任何事情)。假设为了矛盾,该减少的列表的最大子集和包括多于一个条目。所以,让我们用最大子集和表示间隔:
... a1 -b1 a2 -b2 ... a(n-1) b(n-1) an ...
ai
为正数,bi
为负数。 (请注意,由于显而易见的原因,两端都不是负数。)
我们现在将产生矛盾:
首先,注意a1 < b1
,因为否则我们可以将那两个元素从那一端切掉并获得更大的子集和。另外b1 > a2
,因为我们的列表已缩减,如果b1 < a2
,则a1 -b1 a2
会折叠为单个条目(因为我们已经知道b1 < a1
。
接下来,请注意a2 > b2
。这是因为如果a2 < b2
,则此事实与b1 < a2
相结合会导致-b1 a2 -b2
折叠为单个条目,按照第三种简化。
反复应用这些参数,我们得到了一系列不等式:
a1 > b1 > a2 > b2 > ... > a(n-1) > b(n-1) > an
但是最后的不等式是一个矛盾,因为如果b(n-1) > an
,那么我们可以将这两个元素从区间中删除并获得更大的子集和。
(请注意,我忽略了两个相邻的ai
和bj
相等的可能性。你必须要小心谨慎处理这个案例,但是效果很好,并且严格的不平等更容易遵循论证。)
您需要保留原始列表的副本,并且当元素的值发生更改时,应用“以”为中心的简化步骤,以避免重新简化列表中的更多内容。
请注意,您最初需要(实际上,能够)执行的更简化,最终列表越小,以后节省运行时间。可能预期的(聚合)运行时间比O(N^2)
好,但我真的不想考虑它,这是一个分析的毛茸茸的算法。
保留几个最高的最大子集和候选者的排序列表可能是最快的,并且在新元素出现后有选择地更新该列表。
答案 1 :(得分:0)
您可以使用square root decomposition。
将整个数组划分为sqrt(n)
大小的块,并分别为每个块计算答案。更新时,最多只能更新2个块。一旦你这样做,你可以在块数组上应用Kadane的算法(注意,最多有sqrt(n)
块)。因此,您可以通过线性预处理获得O(sqrt(n))
每次更新的时间。
答案 2 :(得分:0)
事实上,我们可以处理任何修改数组元素的更新序列,每次更新的时间为 O(log n),预处理时间为 O(n)。转置只是 2 次更新的序列(先修改一个元素,然后再修改另一个)。
这是使用范围树解决的动态 range minimum query 问题的概括。对于我们的范围树,如果一个节点负责一个区间 I,我们需要存储 4 个值,而不是简单地存储 I 中的最小/最大值:
当 I 为单节点时,很容易计算 S、L、R 和 M。
假设 I 由两个子区间组成:左侧的 I1 和右侧的 I2。显然,S(I) = S(I1) + S(I2)。为了计算 L(I),我们观察到 I 的前缀要么是 I1 的前缀,要么是与 I2 前缀连接的整个 I1。因此
L(I) = max(L(I1), S(I1) + L(I2))
同样
R(I) = max(R(I2), S(I2) + R(I1))
最后,M(I) 要么完全落入 I1 之内,要么完全落入 I2 之内,要么跨越 I1 或 I2 之间的边界。在最后一种情况下,换句话说,我们将 I1 的后缀和 I2 的前缀组合在一起,显然我们要选择 I2 的最佳后缀和 I2 的最佳可能前缀,因此
M(I) = max(M(I1), M(I2), R(I1) + L(I2))
如您所见,为了在我们爬上树时计算 M 值,必须保留所有 4 个值。单独用于子区间的 M 没有足够的信息来计算组合区间的 M。
树中有 O(n) 个节点,计算每个节点需要 O(1) 时间。当执行更新时,它在树中的 O(log n) 个祖先需要更新,然后可以读取根处的 M 值(代表整个数组)。
SPOJ GSS3 是一个类似的问题,其中空子数组无效,因此需要对上述方案稍作修改。