当所有值均为非负数时,我们真的可以避免多余的空间吗?

问题描述

这个问题是我很久以前问过的another one的后续内容

我们得到了一个整数数组和另一个数字k,我们需要找到总和等于k的连续子数组的总数。例如,对于输入[1,1,1]k=2,预期输出2

accepted answer中,@talex说:

PS:BTW如果所有值都不为负,则有更好的算法。它不需要额外的内存。

虽然我当时对此并没有太多考虑,但我对此感到很好奇。恕我直言,我们需要额外的内存。如果所有输入值均为非负数,我们的运行(前缀)总和将继续增加,因此,可以肯定的是,我们不需要unordered_map来存储特定总和的频率。但是,我们仍然需要额外的内存(也许是unordered_set)来存储我们沿途运行的(前缀)和。这显然与@talex所说的相矛盾。

有人可以确认我们是否绝对需要额外的内存,还是可以避免?

谢谢!

解决方法

让我们从一个稍微简单的问题开始:所有值均为正(无零)。在这种情况下,子数组可以重叠,但不能包含彼此。

即:arr = 2 1 5 1 1 5 1 2,总和= 8

2 1 5 1 1 5 1 2
|---|
  |-----|
      |-----|
          |---|

但是这种情况永远不会发生:

* * * * * * *
  |-------|
    |---|

考虑到这一点,有一种算法不需要额外的空间(嗯。O(1)空间)并且具有O(n)的时间复杂度。想法应具有指示当前序列和当前序列之和的左右索引。

  • 如果总和为k,则增加计数器,将leftright前进
  • 如果总和小于k,则提前right
  • 其他推进left

现在,如果零为零,则间隔可以包含另一个,但前提是零位于间隔的边缘。

要适应非负数:

执行上述操作,除了:

  • 前进left时跳过零
  • 如果总和为k
    • 计算right右边连续的零,假设zeroes_right_count
    • 计数left左侧的连续零。可以说zeroes_left_count
    • 而不是像以前那样增加计数,而是将计数器增加:(zeroes_left_count + 1) * (zeroes_right_count + 1)

示例:

... 7 0 0 5  1  2 0 0 0 9 ...
          ^     ^
          left  right         

在这里,我们在左边有2个零,在右边有3个零。这使得(2 + 1) * (3 + 1) = 12序列的总和为8

5 1 2
5 1 2 0
5 1 2 0 0 
5 1 2 0 0 0

0 5 1 2 
0 5 1 2 0
0 5 1 2 0 0 
0 5 1 2 0 0 0

0 0 5 1 2
0 0 5 1 2 0
0 0 5 1 2 0 0 
0 0 5 1 2 0 0 0
,

我认为该算法可以使用O(1)空间来工作。

我们维护两个指针,分别指向当前子序列的开始和结束以及当前子序列的总和。最初,两个指针都指向array[0],并且总和显然设置为array[0]

使结束指针前进(从而将子序列向右扩展),并将总和增加它所指向的值,直到总和超过k。然后前进开始指针(因此从左侧缩小子序列),并减小总和,直到该总和低于k。继续执行此操作,直到结束指针到达数组的末尾。跟踪总和正好为k的次数。