【问题标题】:sum of maximum element of sliding window of length K长度为 K 的滑动窗口的最大元素之和
【发布时间】:2016-02-08 08:36:33
【问题描述】:

最近我遇到了一个问题。算法部分需要计算长度为K的滑动窗口的最大元素之和。其中 K 的范围为 1(数组的 N 长度)。

例如,如果我有一个数组 A 作为5,3,12,4
长度为 1 的滑动窗口:5 + 3 + 12 + 4 = 24
长度为 2 的滑动窗口:5 + 12 + 12 = 29
长度为 3 的滑动窗口:12 + 12 = 24
长度为 4 的滑动窗口:12

Final answer is 24,29,24,12.

我试过这个O(N^2)。对于每个长度为 K 的滑动窗口,我可以计算 O(N) 中的最大值。由于 K 最大为 N。因此,整体复杂度为 O(N^2)
我正在寻找 O(N)O(NlogN) 或类似于此算法的东西,因为 N 可能高达 10^5。
注意:数组中的元素可以大到10^9,所以输出最终答案为模10^9 +7

编辑:我真正想在整个线性时间或 O(NlogN) 中而不是 O(KN) 中找到每个 K 值(即从 0 到 N)的答案) 或 O(KNlogN) 其中 K={1,2,3,.... N}

【问题讨论】:

  • 为什么我的问题中有反对票。请说出原因以便我修改问题
  • 不确定具体情况,但这似乎与一些数学技巧有关。让我们看看项目A[i]=x。我们查看窗口1..Ksize 的所有值。当size=1 时,x 为最大值。当size 增长时,x 可能不再是最大值。一旦它不是最大值(比如size=m),它将不再是最大值。我认为这是解决方案的关键部分。
  • 直觉上这似乎很难:令人惊讶的是,对于特定的 K 可以完成 O(N) 并且需要将 K 硬编码到数据结构中。为所有 K 做 O(N) 似乎非常具有挑战性。另一方面,如果我们可以利用相邻 K 的一些共性,那么 O(NlogN) 可能是可能的吗?
  • @shapiro yaacov 如果给定的数组是升序还是降序呢?那么最坏的情况是O(N ^ 2)。如果你有更好的复杂性,那么请写下这篇文章的答案。
  • @strubbly - 我认为你所说的 如果我们可以利用相邻 K 的一些共性,那么...... 接近我的意思。在查看单个项目及其周围环境时,我们会获得一些关于数组的知识......

标签: algorithm


【解决方案1】:

这是 O(n) 的简略草图。

对于每个元素,确定左侧有多少连续元素不大于(称为a),以及右侧有多少连续元素小于(称为b)。这可以在 O(n) 时间内对所有元素完成 - 请参阅 MBo 的答案。

一个特定元素在其窗口中最大,如果该窗口包含该元素并且仅在其左侧到a 和在其右侧的b 之间的元素。有用的是,长度为 k 的此类窗口的数量(以及这些窗口的总贡献)在 k 中是分段线性的,最多有五个。比如a = 5b = 3,有

1 window  of size 1
2 windows of size 2
3 windows of size 3
4 windows of size 4
4 windows of size 5
4 windows of size 6
3 windows of size 7
2 windows of size 8
1 window  of size 9.

我们需要对这个贡献进行有效编码的数据结构是一棵 Fenwick 树,它的值不是数字,而是 k 的线性函数。对于分段线性贡献函数的每个线性部分,我们将其添加到其间隔开始处的单元格中,并从结尾处的单元格中减去它(闭合开始,开放结束)。最后,我们检索所有前缀和,并在它们的索引 k 处评估它们以获得最终数组。

(好的,现在必须运行,但我们实际上并不需要在第二步中使用 Fenwick 树,这会将复杂度降低到 O(n),并且可能有一种方法可以在线性中执行第一步时间。)

Python 3,经过轻微测试:

def left_extents(lst):
  result = []
  stack = [-1]
  for i in range(len(lst)):
    while stack[-1] >= 0 and lst[i] >= lst[stack[-1]]:
      del stack[-1]
    result.append(stack[-1] + 1)
    stack.append(i)
  return result


def right_extents(lst):
  result = []
  stack = [len(lst)]
  for i in range(len(lst) - 1, -1, -1):
    while stack[-1] < len(lst) and lst[i] > lst[stack[-1]]:
      del stack[-1]
    result.append(stack[-1])
    stack.append(i)
  result.reverse()
  return result


def sliding_window_totals(lst):
  delta_constant = [0] * (len(lst) + 2)
  delta_linear = [0] * (len(lst) + 2)
  for l, i, r in zip(left_extents(lst), range(len(lst)), right_extents(lst)):
    a = i - l
    b = r - (i + 1)
    if a > b:
      a, b = b, a
    delta_linear[1] += lst[i]
    delta_linear[a + 1] -= lst[i]
    delta_constant[a + 1] += lst[i] * (a + 1)
    delta_constant[b + 2] += lst[i] * (b + 1)
    delta_linear[b + 2] -= lst[i]
    delta_linear[a + b + 2] += lst[i]
    delta_constant[a + b + 2] -= lst[i] * (a + 1)
    delta_constant[a + b + 2] -= lst[i] * (b + 1)
  result = []
  constant = 0
  linear = 0
  for j in range(1, len(lst) + 1):
    constant += delta_constant[j]
    linear += delta_linear[j]
    result.append(constant + linear * j)
  return result

print(sliding_window_totals([5, 3, 12, 4]))

【讨论】:

  • 可以在线性时间内完成第一步 - 在我的回答中。但是第二步还不够清楚……
  • 虽然看起来你使用的是分而治之的概念,但我不清楚。
  • @Mike 这不是分而治之。
  • 第二步我看不懂
【解决方案2】:

让我们为每个元素确定一个区间,该元素占主导地位(最大)。我们可以使用堆栈在线性时间内通过向前和向后运行来做到这一点。数组 L 和 R 将包含超出支配区间的索引。

获取左右索引:

Stack.Push(0) //(1st element index)
for i = 1 to Len - 1 do
     while Stack.Peek < X[i] do
         j = Stack.Pop
         R[j] = i   //j-th position is dominated by i-th one from the right
     Stack.Push(i)
while not Stack.Empty
   R[Stack.Pop] = Len  //the rest of elements are not dominated from the right

//now right to left
Stack.Push(Len - 1) //(last element index)
for i = Len - 2 to 0 do
     while Stack.Peek < X[i] do
         j = Stack.Pop
         L[j] = i   //j-th position is dominated by i-th one from the left
     Stack.Push(i)
while not Stack.Empty
   L[Stack.Pop] = -1  //the rest of elements are not dominated from the left

(5,7,3,9,4) 数组的结果。
例如,7 在 0..2 区间占主导地位,9 在 0..4 区间占优势

i  0   1   2   3   4 
X  5   7   3   9   4
R  1   3   3   5   5
L -1  -1   1  -1   4

现在,对于每个元素,我们可以计算它在每个可能的总和中的影响。
元素 5 在 (0,0) 区间占主导地位,仅在 k=1 和项中求和
元素 7 以 (0,2) 间隔占主导地位,在 k=1 和条目中求和一次,在 k=2 条目中求和一次,在 k=3 条目中求和一次。
元素 3 在 (2,2) 区间占主导地位,仅在 k=1 和项中求和
元素 9 在 (0,4) 间隔中占主导地位,它在 k=1 和条目中求和一次,在 k=2 中求和两次,在 k=3 中求和,在 k=4 中求和一次,在 k=5 中求和一次。
元素 4 在 (4,4) 区间占优,仅在 k=1 和项中求和。

一般来说,长数组中心具有长支配间隔的元素可能会在 k 长度总和中放弃 k*Value 影响(它取决于相对于数组末端和另一个 dom.元素的位置)

k    1    2    3     4    5
 --------------------------
     5        
     7    2*7  7
     3    
     9    2*9  2*9   2*9  9
     4
 --------------------------
S(k) 28   32   25    18   9

注意系数的总和是 N*(N-1)/2(等于可能的窗口数),大部分表项是空的,所以复杂度似乎比 O(N^2) 好
(我仍然怀疑确切的复杂性)

【讨论】:

  • 好主意,但仍然是 O(N^2)。
  • 仍然是 O(N^2)。证明:我们保留一个数组,其中包含每个 K 值的最大值小计。对于原始数组中的每个元素,我们必须更新小计数组中 K 的 X 值,其中 X 是支配区间的大小(例如对于 K=1、K=2 和 K=3),一个具有大小为 3 的支配区间的元素将被添加到小计中。所以整体复杂度是所有​​元素的支配区间大小的总和。在最坏的情况下,这是 O(N^2) - 例如,如果原始数组按递增顺序排列,则支配区间大小为 1-N,总和为 O(N^2)。
  • 哟!请参阅上面的证明,为什么您的算法是 O(N^2).... 并不比简单的解决方案更好。事实上,我认为 O(N^2) 是最低限度,就像我在回答中所说的那样。 “(我仍然怀疑确切的复杂性)” - 不用再怀疑.....
  • @gen-y-s 对于增加订单的最坏情况,您是对的。伤心:(
  • “你是对的......” - 谢谢!!!顺便说一句,我不仅是对的,而且当你做不到的时候,我还解决了你的算法复杂性的问题。 QED
【解决方案3】:

对于给定的窗口大小,滑动窗口的最大值之和可以使用双端队列在线性时间内计算,该队列保留当前窗口中的元素。我们维护双端队列,使队列中的第一个(索引 0,最左边)元素始终是当前窗口的最大值。

这是通过遍历数组来完成的,在每次迭代中,首先我们删除双端队列中的第一个元素,如果它不再在当前窗口中(我们通过检查它的原始位置来做到这一点,它也保存在双端队列及其值)。然后,我们从双端队列的末尾删除任何小于当前元素的元素,最后将当前元素添加到双端队列的末尾。

计算所有大小为 K 的滑动窗口的最大值的复杂度为 O(N)。如果您想对从 1..N 的所有 K 值执行此操作,则时间复杂度将为 O(N^2) . O(N) 是计算所有大小为 K 的窗口的最大值之和的最佳时间(很容易看到)。要计算其他 K 值的总和,简单的方法是对每个不同的 K 值重复计算,这将导致 O(N^2) 的总时间。有没有更好的办法 ?不,因为即使我们保存一个 K 值的计算结果,我们也无法在少于 O(N) 的时间内使用它来计算另一个 K 值的结果。所以最好的时间是O(N^2)。

以下是python中的一个实现:

from collections import deque

def slide_win(l, k):
  dq=deque()
  for i in range(len(l)):
    if len(dq)>0 and dq[0][1]<=i-k:
      dq.popleft()
    while len(dq)>0 and l[i]>=dq[-1][0]:
      dq.pop()
    dq.append((l[i],i))
    if i>=k-1:
      yield dq[0][0]

def main():
  l=[5,3,12,4]
  print("l="+str(l))
  for k in range(1, len(l)+1):
    s=0
    for x in slide_win(l,k):
      s+=x
    print("k="+str(k)+" Sum="+str(s))

【讨论】:

  • 请阅读问题。我已经说过我可以在 O(N) 中找到任何 K 的总和。但我想在复杂度 O(N) 或 O(NlogN) 中找到所有 K 的总和
  • 阅读代码——主函数调用slide_win函数获取所有窗口的最大值,然后计算总和。
猜你喜欢
  • 2015-02-28
  • 1970-01-01
  • 1970-01-01
  • 2018-04-20
  • 1970-01-01
  • 2016-01-01
  • 2022-11-17
  • 1970-01-01
  • 2019-08-22
相关资源
最近更新 更多