长度在给定范围内的最长子数组

时间:2018-07-22 12:04:06

标签: algorithm

如果我有一个整数列表,则在数组中,如何找到最长子数组的长度,以使该数组的最小和最大元素之间的差小于给定的整数,例如M。

所以如果我们有一个包含3个元素的数组,

[1、2、4]

如果M等于2

那么最长的替补将是[1,2]

因为如果我们包括4,并且我们从头开始,则差异将是3,大于M(= 2),如果我们从2开始,则最大(4)和最小元素之间的差异(2)为2,且不少于2(M)

我能想到的最好的方法是从左侧开始,然后尽可能远地向右移动,而子数组范围不会太高。当然,在每个步骤中,我们都必须跟踪到目前为止的最小和最大元素。但是,这具有n平方的时间复杂度,我们不能更快地得到它吗?

2 个答案:

答案 0 :(得分:1)

我对David Winder的算法进行了改进。这个想法是,我们可以使用我称为双端队列DP优化技巧(而不是使用两个堆来查找最小和最大元素)(在某个地方可能有个适当的名称)。

要理解这一点,我们可以看一个更简单的问题:在数组中某个大小为k的所有子数组中找到最小元素。这个想法是我们保留一个包含最小元素潜在候选者的双头队列。当遇到一个新元素时,在将当前元素推入后端之前,我们会弹出队列中后端所有大于或等于当前元素的元素。

之所以可以这样做,是因为我们知道将来遇到的任何子数组(包括一个弹出的元素)也将包含当前元素,并且由于当前元素小于弹出的那些元素,因此这些元素永远不会是最小值。

在推送当前元素之后,如果相距k个元素之外,我们会从队列中弹出最前面的元素。当前子数组中的最小元素只是队列中的第一个元素,因为我们从队列后面弹出元素的方式使它不断增加。

要在您的问题中使用此算法,我们将有两个双端队列来存储最小和最大元素。当我们遇到一个比最小元素大得多的新元素时,我们从双端队列的前面弹出,直到该元素不再太大为止。在该位置结束的最长数组的开头就是我们弹出的最后一个元素的索引加1。

这使解决方案O(n)

C ++实现:

int best = -1234567890, beg = 0;
//best = length of the longest subarray that meets the requirements so far
//beg = the beginning of the longest subarray ending at the current index
std::deque<int> least, greatest;
//these two deques store the indices of the elements which could cause trouble
for (int i = 0; i < n; i++)
{
    while (!least.empty() && a[least.back()] >= a[i])
    {
        least.pop_back();
        //we can pop this off since any we encounter subarray which includes this
        //in the future will also include the current element
    }
    least.push_back(i);
    while (!greatest.empty() && a[greatest.back()] <= a[i])
    {
        greatest.pop_back();
        //we can pop this off since any we encounter subarray which includes this
        //in the future will also include the current element
    }
    greatest.push_back(i);
    while (a[least.front()] < a[i] - m)
    {
        beg = least.front() + 1;
        least.pop_front();
        //remove elements from the beginning if they are too small
    }
    while (a[greatest.front()] > a[i] + m)
    {
        beg = greatest.front() + 1;
        greatest.pop_front();
        //remove elements from the beginning if they are too large
    }
    best = std::max(best, i - beg + 1);
}

答案 1 :(得分:0)

考虑以下想法:

让我们创建一个MaxLen数组(大小为n),其定义为:MaxLen [i] =直到第i位的最大子数组的长度。

填充完该数组后,很容易(O(n))找到最大子数组。

我们如何填充MaxLen数组?假设您知道MaxLen [i],MaxLen [i + 1]中将包含什么?

我们有2个选项-如果originalArr [i + 1]中的数字没有打破您在以索引i结尾的最长子数组中m的差异超过diff的约束,那么MaxLen[i+1] = MaxLen[i] + 1(因为我们能够另一方面,如果前一个子数组的前一个子数组稍长一些,如​​果originalArr [i + 1]的值大于或小于diff m,我们需要找到diff的元素为m(索引是k),然后插入MaxLen[i+1] = i - k + 1,因为我们的新的max子数组将必须排除originalArr [k]元素。

我们如何找到这个“坏”元素?我们将使用堆。在传递每个元素之后,我们将其值和索引插入到最小和最大堆中(在log(n)中完成)。当您拥有第i个元素并且要检查之前的最后一个数组中是否有人破坏您的序列时,您可以开始从堆中提取元素,直到没有任何元素变大或变小originalArr [i]->取提取元素的最大索引,而您的k-破坏序列的元素的索引。

我将尝试使用伪代码进行简化(我仅演示了最小堆,但与最大堆相同)

    Array is input array of size n
    min-heap = new heap()
    maxLen = array(n) // of size n
    maxLen[0] = 1; //max subArray for original Array with size 1
    min-heap.push(Array[0], 0)
    for (i in (1,n)) {
         if (Array[i] - min-heap.top < m) // then all good
              maxLen[i] = maxLen[i-1] + 1
         else {
              maxIndex = min-heap.top.index;
              while (Array[i] - min-heap.top.value > m)
                     maxIndex = max (maxIndex , min-heap.pop.index)
                     if (empty(min-heap))
                           maxIndex = i // all element are "bad" so need to start new sub-array
                           break 
              //max index is our k ->
              maxLen[i] = i - k + 1
         } 
         min-heap.push(Array[i], i)

完成后,在最大长度数组上运行并选择最大值(从他的索引中可以提取原始数组的begin-end索引)。

因此,我们遍历了数组(n),并在每个插入中循环了2个堆(log n)。 您可能会说:嗨!但是您也有未知的堆提取时间,这些时间会强制执行heapify(log n)!但是请注意,该堆最多可以包含n个元素,并且元素可以提取两次,因此计算累积复杂度,您将看到其仍为o(1)。 因此,底线是:O(n * logn)。

已编辑:

此解决方案可以通过使用AVL树而不是2个堆来简化-查找min和max都是AVL树中的O(logn)-插入,查找和删除操作都一样-因此只需将树与value和element一起使用在原始数组中有索引。

编辑2:

@Fei Xiang甚至用双端队列提出了O(n)的更好解决方案。