谷歌面试:在给定的整数数组中找到所有连续的子序列,其总和在给定范围内。我们能比 O(n^2) 做得更好吗?

Posted

技术标签:

【中文标题】谷歌面试:在给定的整数数组中找到所有连续的子序列,其总和在给定范围内。我们能比 O(n^2) 做得更好吗?【英文标题】:Google Interview: Find all contiguous subsequence in a given array of integers, whose sum falls in the given range. Can we do better than O(n^2)? 【发布时间】:2014-08-23 23:38:55 【问题描述】:

给定一个整数数组和一个范围(低、高),找出所有 数组中的连续子序列,其总和在范围内。

有没有比 O(n^2) 更好的解决方案?

我尝试了很多,但找不到比 O(n^2) 更好的解决方案。请帮助我找到更好的解决方案或确认这是我们能做的最好的。

这就是我现在所拥有的,我假设范围被定义为[lo, hi]

public static int numOfCombinations(final int[] data, final int lo, final int hi, int beg, int end) 
    int count = 0, sum = data[beg];

    while (beg < data.length && end < data.length) 
       if (sum > hi) 
          break;
        else 
          if (lo <= sum && sum <= hi) 
            System.out.println("Range found: [" + beg + ", " + end + "]");
            ++count;
          
          ++end;
          if (end < data.length) 
             sum += data[end];
          
       
    
    return count;


public static int numOfCombinations(final int[] data, final int lo, final int hi) 
    int count = 0;

    for (int i = 0; i < data.length; ++i) 
        count += numOfCombinations(data, lo, hi, i, i);
    

    return count;

【问题讨论】:

sum &gt; hi .. break 是否假定整数是非负数? (否则,如果总和可以随着我们继续而减少,为什么要打破。) 给定一个全为零的数组和范围 (-1, 1),有 O(n^2) 的解决方案,你显然需要 O(n^2) 的时间来打印答案. @RaymondChen 我认为在他的代码中,他只返回count ? 所有整数都只能是正数吗?或者可以是正面的还是负面的? @notbad 整数可以是正数也可以是负数 【参考方案1】:

O(NlogN) 具有简单的数据结构就足够了。

对于连续的子序列,我认为这意味着对于子数组。

我们维护一个前缀和列表,prefix[i] = sum for the first i elements。如何检查[low, high]之间是否存在范围朗姆酒?我们可以使用二分搜索。所以,

prefix[0] = array[0]  
for i in range(1, N) 
  prefix[i] = array[i] + prefix[i-1];
  idx1 = binarySearch(prefix, prefix[i] - low);
  if (idx1 < 0) idx1 = -1 - idx1;
  idx2 = binarySearch(prefix, prefix[i] - high);
  if (idx2 < 0) idx2 = -1 - idx2;
  // for any k between [idx1, idx2], range [k, i] is within range [low, high]
  insert(prefix, prefix[i])

我们唯一需要注意的是我们还需要插入新值,因此任何数组或链表都可以。我们可以使用 TreeSet,或者实现自己的 AVL 树,二分查找和插入都在 O(logN) 内。

【讨论】:

【参考方案2】:

O(n)时间解:

您可以为问题的“精确”版本扩展“双指针”的想法。我们将维护变量ab 以使xs[i,a), xs[i,a+1), ..., xs[i,b-1) 形式上的所有区间在所寻求的范围[lo, hi] 中都有一个总和。

a, b = 0, 0
for i in range(n):
    while a != (n+1) and sum(xs[i:a]) < lo:
        a += 1
    while b != (n+1) and sum(xs[i:b]) <= hi:
        b += 1
    for j in range(a, b):
        print(xs[i:j])

这实际上是O(n^2),因为sum,但我们可以通过首先计算前缀和ps 来轻松解决这个问题,这样ps[i] = sum(xs[:i])。那么sum(xs[i:j]) 就是ps[j]-ps[i]

这是在[2, 5, 1, 1, 2, 2, 3, 4, 8, 2][lo, hi] = [3, 6] 上运行上述代码的示例:

[5]
[5, 1]
[1, 1, 2]
[1, 1, 2, 2]
[1, 2]
[1, 2, 2]
[2, 2]
[2, 3]
[3]
[4]

这在时间O(n + t) 中运行,其中t 是输出的大小。正如一些人所注意到的,输出可以大到t = n^2,即如果所有连续的子序列都匹配。

如果我们允许以压缩格式写入输出(输出对 a,b,其中所有子序列都是连续的),我们可以得到纯 O(n) 时间算法。

【讨论】:

我认为即使使用 O(1) 空间实际上也可以解决它。我们可以只维护两个和,sum(xs[i:a])sum(xs[i:b]),而不是计算前缀和数组。当起始位置移动时,即i 递增,只需从两个和中减去该值即可。 @RameshwarBhaskaran 不幸的是,是的。对于负数,我们不再保证序列随着 b 增加而随着 a 减少。 你能解释一下这个解决方案背后的直觉吗? 不适用于负数和零数。例如:范围 [15, 20] 的 5, 10, 2, 3, 5, -5。数组中所有元素的总和等于 20,但不会被您的算法捕获。不过,您的算法可以很好地处理正整数。【参考方案3】:

从这个problem开始:找到所有和为x的连续子序列。我们需要的是类似的东西。

对于每个索引i,我们可以计算从0到i的段之和,即x。所以,现在的问题是我们需要找到从 0 到 i - 1,从 (x - low) 到 (x - high) 的总和有多少个段,它应该比 O(n) 快。所以有几个数据结构可以帮助你在 O(logn) 中做到这一点,它们是 Fenwick tree 和 Interval tree。

所以我们需要做的是:

遍历从 0 到 n 的所有索引(n 是数组的大小)。

在索引 ith 处,从 0 开始计算到第 i 个索引的总和 x,查询树得到落在范围(x - 高,x - 低)内的数字的总出现次数。

将 x 添加到树中。

所以时间复杂度是O(n log n)

【讨论】:

区间树和段树是两个不同的东西。 区间树不是你想象的那样。支持所需操作的数据结构是 Fenwick 树和 Segment 树。【参考方案4】:

您应该使用简单的动态规划和二进制搜索。要查找计数:

    from bisect import bisect_left, bisect_right

    def solve(A, start, end):
        """
        O(n lg n) Binary Search
        Bound:
        f[i] - f[j] = start
        f[i] - f[j'] = end
        start < end
        f[j] > f[j']

        :param A: an integer array
        :param start: lower bound
        :param end: upper bound 
        :return:
        """
        n = len(A)
        cnt = 0
        f = [0 for _ in xrange(n+1)]

        for i in xrange(1, n+1):
            f[i] = f[i-1]+A[i-1]  # sum from left

        f.sort()
        for i in xrange(n+1):
            lo = bisect_left(f, f[i]-end, 0, i)
            hi = bisect_right(f, f[i]-start, 0, i)
            cnt += hi-lo

        return cnt

https://github.com/algorhythms/LintCode/blob/master/Subarray%20Sum%20II.py

要查找结果而不是计数,您只需要另一个哈希表来存储原始(未排序) f[i] -> 索引列表的映射。

干杯。

【讨论】:

很好的解决方案!只是 f 可能不需要排序 如果数组包含负数,f需要排序。 @ThinkRecursively 如果数组包含非负数,你只是 sum 数组是单调的,并且不需要排序即可进行二进制搜索,但如果它包含负数,则值可能会下降而且它不是单调的,所以你需要排序,但我不确定算法的其余部分是否适用于负数。 当数组包含负数时不起作用。例如,考虑 [2,-1] 的低 =-1 和高 = 0。有一个子序列 (1,1) 的总和为 -1,但上述算法将返回 0。 @Satvik 如果算法不适用于负数。为什么需要排序?【参考方案5】:
yes in my opinion it can be in O(n)

struct subsequence

int first,last,sum;
s;

function(array,low,high)

int till_max=0;
s.first=0;s.last=0;s.sum=0;
for(i=low;i<high;i++)


if(till_max+array[i]>array[i])

s.first=s.first;
s.last=i;
till_max+=array[i];

else

s.first=i;
s.last=i;
till_max=array[i];

if(till_max in range)

s.sum=till_max;
   printf("print values between first=%d and last=%d and sum=%d",s.first,s.last,s.sum);



【讨论】:

【参考方案6】:

如果所有整数都是非负数,则可以在O(max(size-of-input,size-of-output)) 时间内完成。这是最佳选择。

这是 C 中的算法。

void interview_question (int* a, int N, int lo, int hi)

  int sum_bottom_low = 0, sum_bottom_high = 0,
      bottom_low = 0, bottom_high = 0,
      top = 0;
  int i;

  if (lo == 0) printf ("[0 0) ");
  while (top < N)
  
    sum_bottom_low += a[top];
    sum_bottom_high += a[top];
    top++;
    while (sum_bottom_high >= lo && bottom_high <= top)
    
      sum_bottom_high -= a[bottom_high++];
    
    while (sum_bottom_low > hi && bottom_low <= bottom_high)
    
      sum_bottom_low -= a[bottom_low++];
    
    // print output
    for (i = bottom_low; i < bottom_high; ++i)
      printf ("[%d %d) ", i, top);
  
  printf("\n");

除了最后一个循环标记为“打印输出”外,每个操作都执行O(N)次;对于每个打印的间隔,最后一个循环执行一次。如果我们只需要计算间隔而不打印它们,整个算法就变成了O(N)

如果允许负数,那么O(N^2) 很难被击败(可能是不可能的)。

【讨论】:

【参考方案7】:

如果只有正数,您可以获得 O(nlogn) 的方法:-

1. Evaluate cumulative sum of array
2. for i  find total sum[j] in (sum[i]+low,sum[i]+high) using binary search
3. Total = Total + count
4. do 3 to 5 for all i

时间复杂度:-

Cumulative sum is O(N)
Finding sums in range is O(logN) using binary search
Total Time complexity is O(NlogN)

【讨论】:

二进制搜索?累计总和可能没有排序? @PhamTrung 它仅适用于正整数,请检查

以上是关于谷歌面试:在给定的整数数组中找到所有连续的子序列,其总和在给定范围内。我们能比 O(n^2) 做得更好吗?的主要内容,如果未能解决你的问题,请参考以下文章

674. 最长连续递增序列

LeetCode - 最长连续递增序列

674. 最长连续递增序列(dp)

3590最长连续递增序列

3590最长连续递增序列

271最长连续递增序列