最大化所有可能子数组的特定总和
Posted
技术标签:
【中文标题】最大化所有可能子数组的特定总和【英文标题】:Maximizing a particular sum over all possible subarrays 【发布时间】:2013-02-24 19:43:29 【问题描述】:考虑一个像下面这样的数组:
1, 5, 3, 5, 4, 1
当我们选择一个子数组时,我们将它减少到子数组中的最小数字。例如,子数组5, 3, 5
变为3, 3, 3
。现在,子数组的总和被定义为结果子数组的总和。例如,5, 3, 5
和为 3 + 3 + 3 = 9
。任务是找到可以从任何子数组中得出的最大可能和。对于上述数组,最大和为 12,由子数组5, 3, 5, 4
给出。
有没有可能比O(n2)更及时地解决这个问题?
【问题讨论】:
@icepack 这不是同一个问题,因为总和是用最小元素构建的。 我想知道将非最大值的索引用作切片的“锚点”是否会有所帮助。就像GetNonMaxIndexes(A)
给你NonMaxIndexes[] = 0,2,4,5
那么你只需要做包含这些索引的切片。存储已用作切片起点的索引也可能有意义。
【参考方案1】:
我相信我有一个在 O(n) 时间内运行的算法。我将首先描述算法的未优化版本,然后给出完全优化的版本。
为简单起见,我们首先假设原始数组中的所有值都是不同的。这通常不是真的,但它提供了一个很好的起点。
算法背后的关键观察如下。找到数组中的最小元素,然后将数组拆分为三部分 - 最小值左侧的所有元素、最小值元素本身以及最小值右侧的所有元素。从示意图上看,这看起来像
+-----------------------+-----+-----------------------+
| left values | min | right values |
+-----------------------+-----+-----------------------+
以下是关键观察:如果您采用给出最佳值的子数组,则以下三件事之一必须为真:
-
该数组包含数组中的所有值,包括最小值。它的总值 min * n,其中 n 是元素的数量。
该数组不包括最小元素。在这种情况下,子数组必须完全位于最小值的左侧或右侧,并且不能包含最小值本身。
这给出了一个很好的初始递归算法来解决这个问题:
如果序列为空,则答案为 0。 如果序列非空: 找出序列中的最小值。 返回以下最大值: 最小值左侧子数组的最佳答案。 最小值右侧子数组的最佳答案。 元素数乘以最小值。那么这个算法的效率如何?嗯,这真的取决于最小元素在哪里。如果您考虑一下,我们会进行线性工作以找到最小值,然后将问题分成两个子问题并在每个子问题上递归。这与您在考虑快速排序时得到的重复出现完全相同。这意味着在最好的情况下需要 Θ(n log n) 时间(如果我们总是在每一半的中间有最小元素),但在最坏的情况下需要 Θ(n2 sup>) 时间(如果我们总是在最左边或最右边有最小值。
但是请注意,我们花费的所有精力都用于在每个子数组中找到最小值,这对于 k 个元素需要 O(k) 时间。如果我们可以将其加速到 O(1) 时间会怎样?在这种情况下,我们的算法会做更少的工作。更具体地说,它只会做 O(n) 的工作。这样做的原因如下:每次我们进行递归调用时,我们都会做 O(1) 工作以找到最小元素,然后从数组中删除该元素并递归处理剩余的部分。因此,每个元素最多可以是一个递归调用的最小元素,因此递归调用的总数不能大于元素的数量。这意味着我们最多可以进行 O(n) 次调用,每个调用都进行 O(1) 次工作,这样总共需要 O(1) 次工作。
那么我们究竟是如何获得这种神奇的加速的呢?在这里,我们可以使用一种令人惊讶的通用且未被充分认识的数据结构,称为 Cartesian tree。笛卡尔树是由具有以下属性的元素序列创建的二叉树:
每个节点都小于其子节点,并且 笛卡尔树的中序游走按顺序返回序列中的元素。例如,序列4 6 7 1 5 0 2 8 3
有这个笛卡尔树:
0
/ \
1 2
/ \ \
4 5 3
\ /
6 8
\
7
这就是我们获得魔法的地方。只需查看笛卡尔树的根,我们就可以立即找到序列的最小元素——这只需要 O(1) 时间。一旦我们这样做了,当我们进行递归调用并查看最小元素左侧或右侧的所有元素时,我们只是递归地下降到根节点的左右子树,即意味着我们可以在 O(1) 时间内读取这些子数组的最小元素。漂亮!
真正的美妙之处在于,可以在 O(n) 时间内为包含 n 个元素的序列构造一棵笛卡尔树。该算法详解in this section of the Wikipedia article。这意味着我们可以得到一个超快速的算法来解决您的原始问题,如下所示:
为数组构造一棵笛卡尔树。 使用上述递归算法,但使用笛卡尔树查找最小元素,而不是每次都进行线性扫描。总的来说,这需要 O(n) 时间并使用 O(n) 空间,这比您最初使用的 O(n2) 算法的时间改进。
在讨论开始时,我假设所有数组元素都是不同的,但这并不是必需的。通过将每个节点小于其子节点的要求更改为每个节点不大于其子节点,您仍然可以为其中包含非不同元素的数组构建笛卡尔树。这不会影响算法的正确性或其运行时间;我将把它作为众所周知的“读者练习”。 :-)
这是一个很酷的问题!我希望这会有所帮助!
【讨论】:
+1。笛卡尔树确实不如他们应得的那样广为人知【参考方案2】:假设数字都是非负数,这不就是“最大化直方图中矩形区域”的问题吗?现在已经成名了……
O(n) 解是可能的。这个网站:http://blog.csdn.net/arbuckle/article/details/710988 有很多巧妙的解决方案。
为了详细说明我的想法(可能不正确),请将每个数字视为宽度为 1 的直方图矩形。
通过“最小化”一个子数组 [i,j] 并相加,您基本上得到了直方图中从 i 到 j 的矩形区域。
这曾出现在 SO:Maximize the rectangular area under Histogram,您可以找到代码和解释,以及官方解决方案页面 (http://www.informatik.uni-ulm.de/acm/Locals/2003/html/judge.html) 的链接。
【讨论】:
请多解释一下。 @Makoto:我试图详细说明。如果我的想法是错误的,请告诉我。谢谢。 如果您能从该页面总结一些结果,那就太好了。 @templatetypedef:链接不够吗?为什么要重复努力?您是否担心烂链接?如果是这样,我相信 SO 已经有这个问题了,我可以找到一个链接。愿意解释请求吗?谢谢。顺便说一句,很好的解决方案。我的一个朋友提出了相同的解决方案(不是针对这个,而是针对我所指的最大矩形问题)。 @Knoothe- 这部分是为了避免链接随着时间的推移而失效,但也是对那些在这里浏览答案的人的礼貌。您链接的页面上有很多答案,所以如果有人正在阅读页面上的其他答案,那么能够浏览这个答案并阅读链接所涵盖的内容会很好。我并不打算让我最初的评论过于严厉 - 这只是要求提供一些可能使答案对读者更有用的东西。【参考方案3】:我尝试的以下算法将具有最初用于对数组进行排序的算法的顺序。例如,如果初始数组使用二叉树排序,则最佳情况为 O(n),平均情况为 O(n log n)。
算法要点:
数组已排序。存储排序后的值和相应的旧索引。从相应的旧索引创建二叉搜索树,用于确定它可以向前和向后走多远而不会遇到小于当前值的值,这将导致最大可能的子数组.
我会在问题[1, 5, 3, 5, 4, 1]中用数组说明方法
1 5 3 5 4 1
-------------------------
array indices => 0 1 2 3 4 5
-------------------------
此数组已排序。按升序存储值及其索引,如下所示
1 1 3 4 5 5
-------------------------
original array indices => 0 5 2 4 1 3
(referred as old_index) -------------------------
同时引用值及其旧索引很重要;像一个关联数组;
需要明确的几个术语:
old_index 指的是一个元素对应的原始索引(即原始数组中的索引);
例如,对于元素 4,old_index 为 4; current_index 为 3;
然而,current_index 指的是排序数组中元素的索引; current_array_value 指的是排序后的数组中的当前元素值。
pre 指顺序前驱; succ指的是顺序后继
另外,最小值和最大值可以直接从排序后的数组的第一个和最后一个元素中得到,分别是min_value和max_value;
现在,算法如下,应该在排序数组上执行。
算法:
从最左边的元素开始。
对于排序数组左侧的每个元素,应用此算法
if(element == min_value)
max_sum = element * array_length;
if(max_sum > current_max)
current_max = max_sum;
push current index into the BST;
else if(element == max_value)
//here current index is the index in the sorted array
max_sum = element * (array_length - current_index);
if(max_sum > current_max)
current_max = max_sum;
push current index into the BST;
else
//pseudo code steps to determine maximum possible sub array with the current element
//pre is inorder predecessor and succ is inorder successor
get the inorder predecessor and successor from the BST;
if(pre == NULL)
max_sum = succ * current_array_value;
if(max_sum > current_max)
current_max = max_sum;
else if (succ == NULL)
max_sum = (array_length - pre) - 1) * current_array_value;
if(max_sum > current_max)
current_sum = max_sum;
else
//find the maximum possible sub array streak from the values
max_sum = [((succ - old_index) - 1) + ((old_index - pre) - 1) + 1] * current_array_value;
if(max_sum > current_max)
current_max = max_sum;
例如,
原始数组是
1 5 3 5 4 1
-------------------------
array indices => 0 1 2 3 4 5
-------------------------
排序后的数组是
1 1 3 4 5 5
-------------------------
original array indices => 0 5 2 4 1 3
(referred as old_index) -------------------------
在第一个元素之后:
max_sum = 6 [会减少到 1*6]
0
第二个元素之后:
max_sum = 6 [会减少到 1*6]
0
\
5
第三个元素之后:
0
\
5
/
2
中序遍历结果:0 2 5
应用算法,
max_sum = [((succ - old_index) - 1) + ((old_index - pre) - 1) + 1] * current_array_value;
max_sum = [((5-2)-1) + ((2-0)-1) + 1] * 3 = 12
current_max = 12 [最大可能值]
第四个元素之后:
0
\
5
/
2
\
4
中序遍历结果:0 2 4 5
应用算法,
max_sum = 8 [小于12被丢弃]
第五个元素之后:
max_sum = 10 [减少到2 * 5,因为小于8而丢弃]
在最后一个元素之后:
max_sum = 5 [减少到1 * 5,小于8就丢弃]
此算法将具有最初用于对数组进行排序的算法的顺序。例如,如果初始数组使用二进制排序,则最佳情况为 O(n),平均情况为 O(n log n)。
空间复杂度将为 O(3n) [O(n + n + n),n 用于排序值,另一个 n 用于旧索引,另一个 n 用于构造 BST]。但是,我不确定这一点。感谢您对算法的任何反馈。
【讨论】:
什么是“二进制排序”?我不熟悉这个算法。以上是关于最大化所有可能子数组的特定总和的主要内容,如果未能解决你的问题,请参考以下文章
c_cpp 最大子阵列总和。在具有最大总和的数组(包含至少一个数字)中查找连续的子数组。